1、GroovyWebApplicationContext
在Spring 4.1之前没有提供Web集成的ApplicationContext,在《Spring4新特性——Groovy Bean定义DSL》中我们自己去实现的com.sishuok.spring4.context.support.WebGenricGroovyApplicationContext,而4.1其已经提供了相应实现,直接把《Spring4新特性——Groovy Bean定义DSL》配置中的相应类改掉即可。
2、视图解析器标签
之前我们都是这样定义视图解析器:
- <bean id="mvcVelocityEngine" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
- <property name="resourceLoaderPath" value="/WEB-INF/vm/,classpath:com/github/zhangkaitao" />
- </bean>
- <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
- <property name="prefix" value=""/>
- <property name="suffix" value=".vm"/>
- <property name="cache" value="false"/>
- </bean>
<bean id="mvcVelocityEngine" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer"> <property name="resourceLoaderPath" value="/WEB-INF/vm/,classpath:com/github/zhangkaitao" /> </bean> <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver"> <property name="prefix" value=""/> <property name="suffix" value=".vm"/> <property name="cache" value="false"/> </bean>
而现在我们可以使用MVC标签定义:
- <mvc:velocity-configurer resource-loader-path="/WEB-INF/vm/,classpath:com/github/zhangkaitao"/>
- <mvc:view-resolvers>
- <mvc:velocity cache-views="false" prefix="" suffix=".vm"/>
- </mvc:view-resolvers>
<mvc:velocity-configurer resource-loader-path="/WEB-INF/vm/,classpath:com/github/zhangkaitao"/> <mvc:view-resolvers> <mvc:velocity cache-views="false" prefix="" suffix=".vm"/> </mvc:view-resolvers>
再来看一个更复杂的例子:
- <mvc:velocity-configurer resource-loader-path="/WEB-INF/vm/,classpath:com/github/zhangkaitao"/>
- <mvc:groovy-configurer resource-loader-path="classpath:templates/" cache-templates="false"/>
- <mvc:view-resolvers>
- <mvc:content-negotiation>
- <mvc:default-views>
- <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
- <property name="jsonpParameterNames">
- <set>
- <value>jsonp</value>
- <value>callback</value>
- </set>
- </property>
- </bean>
- </mvc:default-views>
- </mvc:content-negotiation>
- <mvc:velocity cache-views="false" prefix="" suffix=".vm"/>
- <mvc:groovy cache-views="false" suffix=".tpl"/>
- </mvc:view-resolvers>
<mvc:velocity-configurer resource-loader-path="/WEB-INF/vm/,classpath:com/github/zhangkaitao"/> <mvc:groovy-configurer resource-loader-path="classpath:templates/" cache-templates="false"/> <mvc:view-resolvers> <mvc:content-negotiation> <mvc:default-views> <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"> <property name="jsonpParameterNames"> <set> <value>jsonp</value> <value>callback</value> </set> </property> </bean> </mvc:default-views> </mvc:content-negotiation> <mvc:velocity cache-views="false" prefix="" suffix=".vm"/> <mvc:groovy cache-views="false" suffix=".tpl"/> </mvc:view-resolvers>
mvc:content-negotiation用于定义内容协商的视图解析器,且内部可以定义默认视图;然后我们又定义了mvc:velocity和mvc:groovy两个视图解析器;它们会按照顺序进行解析。另外几个视图解析器是:
mvc:freemarker
mvc:bean-name
mvc:jsp
这种方式有一个很大的问题就是只能做默认配置,如果想自定义其属性值就搞不定了,估计当时开发的人考虑不全或没有经验。
3、控制器标签
Spring 4.1提供了更丰富的控制器标签:
3.1、重定向视图控制器标签
- <mvc:redirect-view-controller
- path="/redirect"
- redirect-url="/status"
- context-relative="true"
- status-code="301"
- keep-query-params="true"/>
<mvc:redirect-view-controller path="/redirect" redirect-url="/status" context-relative="true" status-code="301" keep-query-params="true"/>
3.2、状态控制器标签
- <mvc:status-controller path="/status" status-code="200"/>
<mvc:status-controller path="/status" status-code="200"/>
3.3、带状态的视图控制器标签
- <mvc:view-controller path="/error/**" status-code="200"/>
<mvc:view-controller path="/error/**" status-code="200"/>
4、Groovy Template引擎集成
Spring 4.1提供了对Groovy Template模板引擎的集成,其是一种DSL风格的模板引擎,其也是最早在Spring Boot中引入的。
4.1、Spring配置文件
- <mvc:groovy-configurer resource-loader-path="classpath:templates/" cache-templates="false"/>
- <mvc:view-resolvers>
- <mvc:groovy cache-views="false" suffix=".tpl"/>
- </mvc:view-resolvers>
<mvc:groovy-configurer resource-loader-path="classpath:templates/" cache-templates="false"/> <mvc:view-resolvers> <mvc:groovy cache-views="false" suffix=".tpl"/> </mvc:view-resolvers>
4.2、模板heelo.tpl
- yieldUnescaped ‘<!DOCTYPE html>‘
- html {
- head {
- title(‘hello groovy templates‘)
- }
- body {
- div("hello $user.name")
- }
- }
yieldUnescaped ‘<!DOCTYPE html>‘ html { head { title(‘hello groovy templates‘) } body { div("hello $user.name") } }
具体语法请参考官方文档。
5、 Jackson @JsonView支持
可以使用@JsonView来分组渲染JSON数据,按需展示JSON数据。
5.1、模型
- public class User implements Serializable {
- public static interface OnlyIdView {}
- public static interface OnlyNameView {}
- public static interface AllView extends OnlyIdView, OnlyNameView {}
- @JsonView(OnlyIdView.class)
- private Long id;
- @JsonView(OnlyNameView.class)
- private String name;
- ……
- }
public class User implements Serializable { public static interface OnlyIdView {} public static interface OnlyNameView {} public static interface AllView extends OnlyIdView, OnlyNameView {} @JsonView(OnlyIdView.class) private Long id; @JsonView(OnlyNameView.class) private String name; …… }
定义了三个视图:OnlyIdView、OnlyNameView和AllView。
5.2、控制器
- @RestController
- public class JacksonJsonViewController {
- @RequestMapping("/jackson1")
- @JsonView(User.OnlyIdView.class)
- public User test1() {
- return new User(1L, "zhangsan");
- }
- @RequestMapping("/jackson2")
- @JsonView(User.OnlyNameView.class)
- public User test2() {
- return new User(1L, "zhangsan");
- }
- @RequestMapping("/jackson3")
- @JsonView(User.AllView.class) //可以省略
- public User test3() {
- return new User(1L, "zhangsan");
- }
- }
@RestController public class JacksonJsonViewController { @RequestMapping("/jackson1") @JsonView(User.OnlyIdView.class) public User test1() { return new User(1L, "zhangsan"); } @RequestMapping("/jackson2") @JsonView(User.OnlyNameView.class) public User test2() { return new User(1L, "zhangsan"); } @RequestMapping("/jackson3") @JsonView(User.AllView.class) //可以省略 public User test3() { return new User(1L, "zhangsan"); } }
使用@JsonView控制渲染哪些数据。
6、Jsonp支持
6.1、MappingJackson2JsonView提供的支持
- <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
- <property name="jsonpParameterNames">
- <set>
- <value>jsonp</value>
- <value>callback</value>
- </set>
- </property>
- </bean>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"> <property name="jsonpParameterNames"> <set> <value>jsonp</value> <value>callback</value> </set> </property> </bean>
然后访问如http://localhost:8080/json?callback=callback即可得到JSONP响应:callback({"user":{"id":1,"name":"zhangsan"}});。
6.2、对使用HttpMessageConverter的@ResponseBody的支持
- @Order(2)
- @ControllerAdvice(basePackages = "com.github")
- public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
- public JsonpAdvice() {
- super("callback", "jsonp"); //指定jsonpParameterNames
- }
- }
@Order(2) @ControllerAdvice(basePackages = "com.github") public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpAdvice() { super("callback", "jsonp"); //指定jsonpParameterNames } }
访问http://localhost:8080/jackson1?callback=callback即可看到JSONP响应。
@ContollerAdvice的作用请参考《Spring3.2新注解@ControllerAdvice》,basePackages用于指定对哪些包里的Controller起作用。
6.3、ResponseBodyAdvice
我们之前实现的JsonpAdvice其继承自AbstractJsonpResponseBodyAdvice,而AbstractJsonpResponseBodyAdvice继承自ResponseBodyAdvice,其作用是在响应体写出之前做一些处理:
- @Order(1)
- @ControllerAdvice(basePackages = "com.github")
- public class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> {
- @Override
- public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
- return methodParameter.getMethod().getReturnType().isAssignableFrom(User.class);
- }
- @Override
- public Object beforeBodyWrite(
- Object obj, MethodParameter methodParameter, MediaType mediaType,
- Class<? extends HttpMessageConverter<?>> converterType,
- ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
- User user = ((User)obj);
- user.setName("---" + user.getName() + "---");
- return user;
- }
- }
@Order(1) @ControllerAdvice(basePackages = "com.github") public class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.getMethod().getReturnType().isAssignableFrom(User.class); } @Override public Object beforeBodyWrite( Object obj, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { User user = ((User)obj); user.setName("---" + user.getName() + "---"); return user; } }
1、supports指定支持哪些类型的方法进行处理,此处是返回值为User的;2、我们得到User对象然后在名字前后拼上”---“ ;3、可以指定多个ResponseBodyAdvice,使用@Order指定顺序。访问http://localhost:8080/jackson2?callback=callback可以看到效果。
7、Gson HttpMessageConverter
7.1、Spring配置
- <mvc:annotation-driven>
- <mvc:message-converters>
- <bean class="org.springframework.http.converter.json.GsonHttpMessageConverter"/>
- </mvc:message-converters>
- </mvc:annotation-driven>
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.GsonHttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven>
使用方式和Jackson Json类似。本文使用的是<gson.version>2.2.4</gson.version>版本。
8、Protobuf HttpMessageConverter
8.1、Spring配置
- <mvc:annotation-driven>
- <mvc:message-converters>
- <bean class="org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter">
- <constructor-arg>
- <bean class="com.github.zhangkaitao.web.controller.MyExtensionRegistryInitializer"/>
- </constructor-arg>
- </bean>
- </mvc:message-converters>
- </mvc:annotation-driven>
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter"> <constructor-arg> <bean class="com.github.zhangkaitao.web.controller.MyExtensionRegistryInitializer"/> </constructor-arg> </bean> </mvc:message-converters> </mvc:annotation-driven>
8.2、定义protobuf message(proto/user.proto)
- package com.github.zhangkaitao.pb;
- option java_package = "com.github.zhangkaitao.pb";
- option java_outer_classname = "UserProtos";
- message User {
- optional int64 id = 1;
- optional string name = 2;
- }
package com.github.zhangkaitao.pb; option java_package = "com.github.zhangkaitao.pb"; option java_outer_classname = "UserProtos"; message User { optional int64 id = 1; optional string name = 2; }
8.3、添加maven插件自动把protobuf message转化成Java代码
- <plugin>
- <groupId>com.google.protobuf.tools</groupId>
- <artifactId>maven-protoc-plugin</artifactId>
- <version>0.1.10</version>
- <executions>
- <execution>
- <id>generate-sources</id>
- <goals>
- <goal>compile</goal>
- </goals>
- <phase>generate-sources</phase>
- <configuration>
- <protoSourceRoot>${basedir}/src/main/proto/</protoSourceRoot>
- <includes>
- <param>**/*.proto</param>
- </includes>
- </configuration>
- </execution>
- </executions>
- <configuration>
- <protocExecutable>D:/software/protoc.exe</protocExecutable>
- </configuration>
- </plugin>
<plugin> <groupId>com.google.protobuf.tools</groupId> <artifactId>maven-protoc-plugin</artifactId> <version>0.1.10</version> <executions> <execution> <id>generate-sources</id> <goals> <goal>compile</goal> </goals> <phase>generate-sources</phase> <configuration> <protoSourceRoot>${basedir}/src/main/proto/</protoSourceRoot> <includes> <param>**/*.proto</param> </includes> </configuration> </execution> </executions> <configuration> <protocExecutable>D:/software/protoc.exe</protocExecutable> </configuration> </plugin>
8.4、测试控制器
- @RestController
- public class ProtobufController {
- @RequestMapping("/proto/read")
- public ResponseEntity<UserProtos.User> protoRead() {
- return ResponseEntity.ok(UserProtos.User.newBuilder().setId(1).setName("zhangsan").build());
- }
- @RequestMapping("/proto/write")
- public ResponseEntity<UserProtos.User> protoRead(RequestEntity<UserProtos.User> requestEntity) {
- System.out.println("server===\n" + requestEntity.getBody());
- return ResponseEntity.ok(requestEntity.getBody());
- }
- }
@RestController public class ProtobufController { @RequestMapping("/proto/read") public ResponseEntity<UserProtos.User> protoRead() { return ResponseEntity.ok(UserProtos.User.newBuilder().setId(1).setName("zhangsan").build()); } @RequestMapping("/proto/write") public ResponseEntity<UserProtos.User> protoRead(RequestEntity<UserProtos.User> requestEntity) { System.out.println("server===\n" + requestEntity.getBody()); return ResponseEntity.ok(requestEntity.getBody()); } }
8.5、测试用例(com.github.zhangkaitao.proto.ProtoTest)
- @Test
- public void testRead() {
- HttpHeaders headers = new HttpHeaders();
- RequestEntity<UserProtos.User> requestEntity =
- new RequestEntity<UserProtos.User>(headers, HttpMethod.POST, URI.create(baseUri + "/proto/read"));
- ResponseEntity<UserProtos.User> responseEntity =
- restTemplate.exchange(requestEntity, UserProtos.User.class);
- System.out.println(responseEntity.getBody());
- }
- @Test
- public void testWrite() {
- UserProtos.User user = UserProtos.User.newBuilder().setId(1).setName("zhangsan").build();
- HttpHeaders headers = new HttpHeaders();
- RequestEntity<UserProtos.User> requestEntity =
- new RequestEntity<UserProtos.User>(user, headers, HttpMethod.POST, URI.create(baseUri + "/proto/write"));
- ResponseEntity<UserProtos.User> responseEntity =
- restTemplate.exchange(requestEntity, UserProtos.User.class);
- System.out.println(responseEntity.getBody());
- }
@Test public void testRead() { HttpHeaders headers = new HttpHeaders(); RequestEntity<UserProtos.User> requestEntity = new RequestEntity<UserProtos.User>(headers, HttpMethod.POST, URI.create(baseUri + "/proto/read")); ResponseEntity<UserProtos.User> responseEntity = restTemplate.exchange(requestEntity, UserProtos.User.class); System.out.println(responseEntity.getBody()); } @Test public void testWrite() { UserProtos.User user = UserProtos.User.newBuilder().setId(1).setName("zhangsan").build(); HttpHeaders headers = new HttpHeaders(); RequestEntity<UserProtos.User> requestEntity = new RequestEntity<UserProtos.User>(user, headers, HttpMethod.POST, URI.create(baseUri + "/proto/write")); ResponseEntity<UserProtos.User> responseEntity = restTemplate.exchange(requestEntity, UserProtos.User.class); System.out.println(responseEntity.getBody()); }
测试用例知识请参考《Spring MVC测试框架详解——服务端测试》和《Spring MVC测试框架详解——客户端测试》。
测试过程中会抛出:
- Caused by: java.lang.UnsupportedOperationException
- at java.util.Collections$UnmodifiableMap.put(Collections.java:1342)
- at org.springframework.http.HttpHeaders.set(HttpHeaders.java:869)
- at org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter.setProtoHeader(ProtobufHttpMessageConverter.java:196)
Caused by: java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1342) at org.springframework.http.HttpHeaders.set(HttpHeaders.java:869) at org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter.setProtoHeader(ProtobufHttpMessageConverter.java:196)
这是因为ProtobufHttpMessageConverter会修改响应头,但是ResponseEntity构造时HttpHeaders是不允许修改的。暂时解决办法是注释掉:
- //setProtoHeader(outputMessage, message);
//setProtoHeader(outputMessage, message);
9、RequestEntity/ResponseEntity
Spring 4.1提供了ResponseEntity配对的RequestEntity,使用方式和HttpEntity一样。具体可以参考com.github.zhangkaitao.web.controller.RequestResponseEntityController。
10、MvcUriComponentsBuilder
其作用可以参考《Spring4新特性——注解、脚本、任务、MVC等其他特性改进》,Spring 4.1又提供了一个新的方法MvcUriComponentsBuilder.fromMappingName用于根据控制器方法来生成请求URI。
Java代码
- @RestController
- public class MvcUriComponentsBuilderController {
- @RequestMapping("/uri")
- public String mvcUriComponentsBuilder1() {
- return MvcUriComponentsBuilder.fromMappingName("MUCBC#mvcUriComponentsBuilder1").build();
- }
- @RequestMapping("/uri/{id}")
- public String mvcUriComponentsBuilder2(@PathVariable Long id) {
- return MvcUriComponentsBuilder.fromMappingName("MUCBC#mvcUriComponentsBuilder2").arg(0, "123").build();
- }
- }
@RestController public class MvcUriComponentsBuilderController { @RequestMapping("/uri") public String mvcUriComponentsBuilder1() { return MvcUriComponentsBuilder.fromMappingName("MUCBC#mvcUriComponentsBuilder1").build(); } @RequestMapping("/uri/{id}") public String mvcUriComponentsBuilder2(@PathVariable Long id) { return MvcUriComponentsBuilder.fromMappingName("MUCBC#mvcUriComponentsBuilder2").arg(0, "123").build(); } }
规则是“控制器所有大写字母#方法名”找到相应的方法。 另外可以直接在页面中使用如下方式获取相应的URI:
- ${s:mvcUrl(‘MUCBC#mvcUriComponentsBuilder2‘).arg(0,"123").build()}
${s:mvcUrl(‘MUCBC#mvcUriComponentsBuilder2‘).arg(0,"123").build()}
如上方式只能在正常EL 3.0的容器中运行,可参考《Expression Language 3.0新特性》。
11、MockRestServiceServer
MockRestServiceServer目前提供了对AsyncRestTemplate的支持,使用方式和RestTemplate一样。可参考《Spring MVC测试框架详解——客户端测试》。
12、MockMvcConfigurer
Spring 4.1提供了MockMvcConfigurer用于进行一些通用配置,使用方式如下:
- mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(defaultSetup()).build();
mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(defaultSetup()).build();
MockMvcConfigurer实现:
- private MockMvcConfigurer defaultSetup() {
- return new MockMvcConfigurer() {
- @Override
- public void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> configurableMockMvcBuilder) {
- configurableMockMvcBuilder.alwaysExpect(status().isOk());
- }
- @Override
- public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> configurableMockMvcBuilder, WebApplicationContext webApplicationContext) {
- return new RequestPostProcessor() {
- @Override
- public MockHttpServletRequest postProcessRequest(MockHttpServletRequest mockHttpServletRequest) {
- mockHttpServletRequest.setAttribute("aa", "aa");
- return mockHttpServletRequest;
- }
- };
- }
- };
- }
private MockMvcConfigurer defaultSetup() { return new MockMvcConfigurer() { @Override public void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> configurableMockMvcBuilder) { configurableMockMvcBuilder.alwaysExpect(status().isOk()); } @Override public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> configurableMockMvcBuilder, WebApplicationContext webApplicationContext) { return new RequestPostProcessor() { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest mockHttpServletRequest) { mockHttpServletRequest.setAttribute("aa", "aa"); return mockHttpServletRequest; } }; } }; }
可以在如上实现中进行一些通用配置,如安全(往Request中扔安全对象之类的)。测试用例可参考com.github.zhangkaitao.proto.ProtoTest2。