Spring4新特性

一、Spring4新特性——泛型限定式依赖注入:

Spring 4.0已经发布RELEASE版本,不仅支持Java8,而且向下兼容到JavaSE6/JavaEE6,并移出了相关废弃类,新添加如Java8的支持、Groovy式Bean定义DSL、对核心容器进行增强、对Web框架的增强、Websocket模块的实现、测试的增强等。其中两个我一直想要的增强就是:支持泛型依赖注入、对cglib类代理不再要求必须有空参构造器了。具体更新请参考:

http://docs.spring.io/spring/docs/4.0.0.RELEASE/spring-framework-reference/htmlsingle/#new-in-4.0

1、相关代码:

1.1、实体

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.     private Long id;  
  3.     private String name;  
  4. }  
  5.   
  6. public class Organization implements Serializable {  
  7.     private Long id;  
  8.     private String name;  
  9. }  

 1.2、Repository

Java代码  收藏代码
  1. public abstract class BaseRepository<M extends Serializable> {  
  2.     public void save(M m) {  
  3.         System.out.println("=====repository save:" + m);  
  4.     }  
  5. }  
  6.   
  7. @Repository  
  8. public class UserRepository extends BaseRepository<User> {  
  9. }  
  10.   
  11. @Repository  
  12. public class OrganizationRepository extends BaseRepository<Organization> {  
  13. }  

 对于Repository,我们一般是这样实现的:首先写一个模板父类,把通用的crud等代码放在BaseRepository;然后子类继承后,只需要添加额外的实现。

 

1.3、Service

1.3.1、以前Service写法

Java代码  收藏代码
  1. public abstract class BaseService<M extends Serializable> {  
  2.     private BaseRepository<M> repository;  
  3.     public void setRepository(BaseRepository<M> repository) {  
  4.         this.repository = repository;  
  5.     }  
  6.     public void save(M m) {  
  7.         repository.save(m);  
  8.     }  
  9. }  
  10. @Service  
  11. public class UserService extends BaseService<User> {  
  12.     @Autowired  
  13.     public void setUserRepository(UserRepository userRepository) {  
  14.         setRepository(userRepository);  
  15.     }  
  16. }  
  17.   
  18. @Service  
  19. public class OrganizationService extends BaseService<Organization> {  
  20.     @Autowired  
  21.     public void setOrganizationRepository(OrganizationRepository organizationRepository) {  
  22.         setRepository(organizationRepository);  
  23.     }  
  24. }  

 

可以看到,以前必须再写一个setter方法,然后指定注入的具体类型,然后进行注入;

 

1.3.2、泛型Service的写法

Java代码  收藏代码
  1. public abstract class BaseService<M extends Serializable> {  
  2.     @Autowired  
  3.     protected BaseRepository<M> repository;  
  4.   
  5.     public void save(M m) {  
  6.         repository.save(m);  
  7.     }  
  8. }  
  9.   
  10. @Service  
  11. public class UserService extends BaseService<User> {  
  12. }  
  13.   
  14. @Service  
  15. public class OrganizationService extends BaseService<Organization> {  
  16. }  

 

 大家可以看到,现在的写法非常简洁。支持泛型式依赖注入。

 

这个也是我之前非常想要的一个功能,这样对于那些基本的CRUD式代码,可以简化更多的代码。

 

 

如果大家用过Spring data jpa的话,以后注入的话也可以使用泛型限定式依赖注入 :

Java代码  收藏代码
  1. @Autowired  
  2. private Repository<User> userRepository;  

 

 对于泛型依赖注入,最好使用setter注入,这样万一子类想变,比较容易切换。比如https://github.com/zhangkaitao/es,如果有多个实现时,子类可以使用@Qualifier指定使用哪一个。

二、Spring4新特性——核心容器的其他改进

1、Map依赖注入:

Java代码  收藏代码
  1. @Autowired  
  2. private Map<String, BaseService> map;  

这样会注入:key是bean名字;value就是所有实现了BaseService的Bean,假设使用上一篇的例子,则会得到:

{organizationService=com.sishuok.spring4.service.OrganizationService@617029, userService=com.sishuok.spring4.service.UserService@10ac73b}

 

2、List/数组注入:

Java代码  收藏代码
  1. @Autowired  
  2. private List<BaseService> list;  

 这样会注入所有实现了BaseService的Bean;但是顺序是不确定的,如果我们想要按照某个顺序获取;在Spring4中可以使用@Order或实现Ordered接口来实现,如:

Java代码  收藏代码
  1. @Order(value = 1)  
  2. @Service  
  3. public class UserService extends BaseService<User> {  
  4. }  

这种方式在一些需要多态的场景下是非常有用的。

 

3、@Lazy可以延迟依赖注入:

Java代码  收藏代码
  1. @Lazy  
  2. @Service  
  3. public class UserService extends BaseService<User> {  
  4. }  
Java代码  收藏代码
  1. @Lazy  
  2. @Autowired  
  3. private UserService userService;  

 我们可以把@Lazy放在@Autowired之上,即依赖注入也是延迟的;当我们调用userService时才会注入。即延迟依赖注入到使用时。同样适用于@Bean。

 

4、@Conditional

@Conditional类似于@Profile(一般用于如我们有开发环境、测试环境、正式机环境,为了方便切换不同的环境可以使用@Profile指定各个环境的配置,然后通过某个配置来开启某一个环境,方便切换,但是@Conditional的优点是允许自己定义规则。可以指定在如@Component、@Bean、@Configuration等注解的类上,以绝对Bean是否创建等。首先来看看使用@Profile的用例,假设我们有个用户模块:

1、在测试/开发期间调用本机的模拟接口方便开发;

2、在部署到正式机时换成调用远程接口;

Java代码  收藏代码
  1. public abstract class UserService extends BaseService<User> {  
  2. }  
  3.   
  4. @Profile("local")  
  5. @Service  
  6. public class LocalUserService extends UserService {  
  7. }  
  8.   
  9. @Profile("remote")  
  10. @Service  
  11. public class RemoteUserService extends UserService {  
  12. }  

我们在写测试用例时,可以指定我们使用哪个Profile:

Java代码  收藏代码
  1. @ActiveProfiles("remote")  
  2. @RunWith(SpringJUnit4ClassRunner.class)  
  3. @ContextConfiguration(locations =  "classpath:spring-config.xml")  
  4. public class ServiceTest {  
  5.   
  6.     @Autowired  
  7.     private UserService userService;  
  8. }  

  这种方式非常简单。如果想自定义如@Profile之类的注解等,那么@Conditional就派上用场了;假设我们系统中有好多本地/远程接口,那么我们定义两个注解@Local和@Remote注解要比使用@Profile方便的多;如:

 

Java代码  收藏代码
  1. @Retention(RetentionPolicy.RUNTIME)  
  2. @Target({ElementType.TYPE, ElementType.METHOD})  
  3. @Conditional(CustomCondition.class)  
  4. public @interface Local {  
  5. }  
  6.   
  7. @Retention(RetentionPolicy.RUNTIME)  
  8. @Target({ElementType.TYPE, ElementType.METHOD})  
  9. @Conditional(CustomCondition.class)  
  10. public @interface Remote {  
  11. }  

 

Java代码  收藏代码
  1. public class CustomCondition implements Condition {  
  2.   
  3.     @Override  
  4.     public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  
  5.         boolean isLocalBean = metadata.isAnnotated("com.sishuok.spring4.annotation.Local");  
  6.         boolean isRemoteBean = metadata.isAnnotated("com.sishuok.spring4.annotation.Remote");  
  7.         //如果bean没有注解@Local或@Remote,返回true,表示创建Bean  
  8.         if(!isLocalBean && !isRemoteBean) {  
  9.             return true;  
  10.         }  
  11.   
  12.         boolean isLocalProfile = context.getEnvironment().acceptsProfiles("local");  
  13.   
  14.         //如果profile=local 且 bean注解了@Local,则返回true 表示创建bean;  
  15.         if(isLocalProfile) {  
  16.             return isLocalBean;  
  17.         }  
  18.   
  19.         //否则默认返回注解了@Remote或没有注解@Remote的Bean  
  20.         return isRemoteBean;  
  21.     }  
  22. }  

 

 然后我们使用这两个注解分别注解我们的Service:

Java代码  收藏代码
  1. @Local  
  2. @Service  
  3. public class LocalUserService extends UserService {  
  4. }  

 

Java代码  收藏代码
  1. @Remote  
  2. @Service  
  3. public class RemoteUserService extends UserService {  
  4. }  

 

首先在@Local和@Remote注解上使用@Conditional(CustomCondition.class)指定条件,然后使用@Local和@Remote注解我们的Service,这样当加载Service时,会先执行条件然后判断是否加载为Bean。@Profile就是这样实现的,其Condition是:org.springframework.context.annotation.ProfileCondition。可以去看下源码,很简单。

 

5、基于CGLIB的类代理不再要求类必须有空参构造器了:

这是一个很好的特性,使用构造器注入有很多好处,比如可以只在创建Bean时注入依赖,然后就不变了,如果使用setter注入,是允许别人改的。当然我们可以使用spring的字段级别注入。如果大家使用过如Shiro,我们可能要对Controller加代理。如果是类级别代理,此时要求Controller必须有空参构造器,有时候挺烦人的。spring如何实现的呢?其内联了objenesis类库,通过它来实现,可以去其官网看看介绍。这样就支持如下方式的构造器注入了:

 

Java代码  收藏代码
  1. @Controller  
  2. public class UserController {  
  3.     private UserService userService;  
  4.     @Autowired  
  5.     public UserController(UserService userService) {  
  6.         this.userService = userService;  
  7.     }  
  8. }  

 

org.springframework.cglib.proxy.Enhancer在其github和maven仓库中的source中竟然木有,其github:https://github.com/spring-projects/spring-framework/tree/master/spring-core/src/main/java/org/springframework/cglib;难道忘了吗?

 

三、Spring4新特性——Web开发的增强

从Spring4开始,Spring以Servlet3为进行开发,如果用Spring MVC 测试框架的话需要指定Servlet3兼容的jar包(因为其Mock的对象都是基于Servlet3的)。另外为了方便Rest开发,通过新的@RestController指定在控制器上,这样就不需要在每个@RequestMapping方法上加 @ResponseBody了。而且添加了一个AsyncRestTemplate ,支持REST客户端的异步无阻塞支持。

 

1、@RestController

Java代码  收藏代码
  1. @RestController  
  2. public class UserController {  
  3.     private UserService userService;  
  4.     @Autowired  
  5.     public UserController(UserService userService) {  
  6.         this.userService = userService;  
  7.     }  
  8.     @RequestMapping("/test")  
  9.       public User view() {  
  10.         User user = new User();  
  11.         user.setId(1L);  
  12.         user.setName("haha");  
  13.         return user;  
  14.     }  
  15.   
  16.     @RequestMapping("/test2")  
  17.     public String view2() {  
  18.         return "{\"id\" : 1}";  
  19.     }  
  20. }  

 其实现就是在@@RestController中加入@ResponseBody:

Java代码  收藏代码
  1. @org.springframework.stereotype.Controller  
  2. @org.springframework.web.bind.annotation.ResponseBody  
  3. public @interface RestController {  
  4. }  

这样当你开发Rest服务器端的时候,spring-mvc配置文件需要的代码极少,可能就仅需如下一行:

Java代码  收藏代码
  1. <context:component-scan base-package="com.sishuok.spring4"/>  
  2. <mvc:annotation-driven/>  

  

2、mvc:annotation-driven配置变化

统一风格;将 enableMatrixVariables改为enable-matrix-variables属性;将ignoreDefaultModelOnRedirect改为ignore-default-model-on-redirect。

 

3、提供AsyncRestTemplate用于客户端非阻塞异步支持。

3.1、服务器端

对于服务器端的springmvc开发可以参考https://github.com/zhangkaitao/servlet3-showcase中的chapter3-springmvc

Java代码  收藏代码
  1. @RestController  
  2. public class UserController {  
  3.     private UserService userService;  
  4.     @Autowired  
  5.     public UserController(UserService userService) {  
  6.         this.userService = userService;  
  7.     }  
  8.     @RequestMapping("/api")  
  9.       public Callable<User> api() {  
  10.         System.out.println("=====hello");  
  11.         return new Callable<User>() {  
  12.             @Override  
  13.             public User call() throws Exception {  
  14.                 Thread.sleep(10L * 1000); //暂停两秒  
  15.                 User user = new User();  
  16.                 user.setId(1L);  
  17.                 user.setName("haha");  
  18.                 return user;  
  19.             }  
  20.         };  
  21.     }  
  22. }  

非常简单,服务器端暂停10秒再返回结果(但是服务器也是非阻塞的)。具体参考我github上的代码。

 

3.2、客户端

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.     AsyncRestTemplate template = new AsyncRestTemplate();  
  3.     //调用完后立即返回(没有阻塞)  
  4.     ListenableFuture<ResponseEntity<User>> future = template.getForEntity("http://localhost:9080/spring4/api", User.class);  
  5.     //设置异步回调  
  6.     future.addCallback(new ListenableFutureCallback<ResponseEntity<User>>() {  
  7.         @Override  
  8.         public void onSuccess(ResponseEntity<User> result) {  
  9.             System.out.println("======client get result : " + result.getBody());  
  10.         }  
  11.   
  12.         @Override  
  13.         public void onFailure(Throwable t) {  
  14.             System.out.println("======client failure : " + t);  
  15.         }  
  16.     });  
  17.     System.out.println("==no wait");  
  18. }  

 此处使用Future来完成非阻塞,这样的话我们也需要给它一个回调接口来拿结果; Future和Callable是一对,一个消费结果,一个产生结果。调用完模板后会立即返回,不会阻塞;有结果时会调用其回调。

 

AsyncRestTemplate默认使用SimpleClientHttpRequestFactory,即通过java.net.HttpURLConnection实现;另外我们也可以使用apache的http components;使用template.setAsyncRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());设置即可。

 

另外在开发时尽量不要自己注册如:

Java代码  收藏代码
  1. <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>  
  2. <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">  

尽量使用

Java代码  收藏代码
  1. <mvc:annotation-driven/>   

它设计的已经足够好,使用子元素可以配置我们需要的配置。

  

且不要使用老版本的:

Java代码  收藏代码
  1. <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>  
  2. <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">  

否则可能得到如下异常:

写道
Circular view path [login]: would dispatch back to the current handler URL [/spring4/login] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)

  

四、Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC :

在之前的《跟我学SpringMVC》中的《第七章 注解式控制器的数据验证、类型转换及格式化》中已经介绍过SpringMVC集成Bean Validation 1.0(JSR-303),目前Bean Validation最新版本是Bean Validation 1.1(JSR-349),新特性可以到官网查看,笔者最喜欢的两个特性是:跨参数验证(比如密码和确认密码的验证)和支持在消息中使用EL表达式,其他的还有如方法参数/返回值验证、CDI和依赖注入、分组转换等。对于方法参数/返回值验证,大家可以参阅《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》。

 

Bean Validation 1.1当前实现是Hibernate validator 5,且spring4才支持。接下来我们从以下几个方法讲解Bean Validation 1.1,当然不一定是新特性:

  1.  集成Bean Validation 1.1到SpringMVC
  2.  分组验证、分组顺序及级联验证
  3.  消息中使用EL表达式
  4.  方法参数/返回值验证
  5.  自定义验证规则
  6.  类级别验证器
  7.  脚本验证器
  8.  cross-parameter,跨参数验证
  9. 混合类级别验证器和跨参数验证器
  10. 组合多个验证注解
  11. 本地化

因为大多数时候验证都配合web框架使用,而且很多朋友都咨询过如分组/跨参数验证,所以本文介绍下这些,且是和SpringMVC框架集成的例子,其他使用方式(比如集成到JPA中)可以参考其官方文档:

规范:http://beanvalidation.org/1.1/spec/

hibernate validator文档:http://hibernate.org/validator/ 

 

 1、集成Bean Validation 1.1到SpringMVC

1.1、项目搭建

首先添加hibernate validator 5依赖:

Java代码  收藏代码
  1. <dependency>  
  2.     <groupId>org.hibernate</groupId>  
  3.     <artifactId>hibernate-validator</artifactId>  
  4.     <version>5.0.2.Final</version>  
  5. </dependency>  

如果想在消息中使用EL表达式,请确保EL表达式版本是 2.2或以上,如使用Tomcat6,请到Tomcat7中拷贝相应的EL jar包到Tomcat6中。

Java代码  收藏代码
  1. <dependency>  
  2.     <groupId>javax.el</groupId>  
  3.     <artifactId>javax.el-api</artifactId>  
  4.     <version>2.2.4</version>  
  5.     <scope>provided</scope>  
  6. </dependency>  

请确保您使用的Web容器有相应版本的el jar包。

 

对于其他POM依赖请下载附件中的项目参考。

 

1.2、Spring MVC配置文件(spring-mvc.xml):

Java代码  收藏代码
  1. <!-- 指定自己定义的validator -->  
  2. <mvc:annotation-driven validator="validator"/>  
  3.   
  4. <!-- 以下 validator  ConversionService 在使用 mvc:annotation-driven 会 自动注册-->  
  5. <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">  
  6.     <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>  
  7.     <!-- 如果不加默认到 使用classpath下的 ValidationMessages.properties -->  
  8.     <property name="validationMessageSource" ref="messageSource"/>  
  9. </bean>  
  10.   
  11. <!-- 国际化的消息资源文件(本系统中主要用于显示/错误消息定制) -->  
  12. <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">  
  13.     <property name="basenames">  
  14.         <list>  
  15.             <!-- 在web环境中一定要定位到classpath 否则默认到当前web应用下找  -->  
  16.             <value>classpath:messages</value>  
  17.             <value>classpath:org/hibernate/validator/ValidationMessages</value>  
  18.         </list>  
  19.     </property>  
  20.     <property name="useCodeAsDefaultMessage" value="false"/>  
  21.     <property name="defaultEncoding" value="UTF-8"/>  
  22.     <property name="cacheSeconds" value="60"/>  
  23. </bean>  

此处主要把bean validation的消息查找委托给spring的messageSource。

 

1.3、实体验证注解:

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.     @NotNull(message = "{user.id.null}")  
  3.     private Long id;  
  4.   
  5.     @NotEmpty(message = "{user.name.null}")  
  6.     @Length(min = 5, max = 20, message = "{user.name.length.illegal}")  
  7.     @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}")  
  8.     private String name;  
  9.   
  10.     @NotNull(message = "{user.password.null}")  
  11.     private String password;  
  12. }  

对于验证规则可以参考官方文档,或者《第七章 注解式控制器的数据验证、类型转换及格式化》。

 

1.4、错误消息文件messages.properties:

Java代码  收藏代码
  1. user.id.null=用户编号不能为空  
  2. user.name.null=用户名不能为空  
  3. user.name.length.illegal=用户名长度必须在520之间  
  4. user.name.illegal=用户名必须是字母  
  5. user.password.null=密码不能为空  

 

1.5、控制器

Java代码  收藏代码
  1. @Controller  
  2. public class UserController {  
  3.   
  4.     @RequestMapping("/save")  
  5.     public String save(@Valid User user, BindingResult result) {  
  6.         if(result.hasErrors()) {  
  7.             return "error";  
  8.         }  
  9.         return "success";  
  10.     }  
  11. }  

 

1.6、错误页面:

Java代码  收藏代码
  1. <spring:hasBindErrors name="user">  
  2.     <c:if test="${errors.fieldErrorCount > 0}">  
  3.         字段错误:<br/>  
  4.         <c:forEach items="${errors.fieldErrors}" var="error">  
  5.             <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/>  
  6.             ${error.field}------${message}<br/>  
  7.         </c:forEach>  
  8.     </c:if>  
  9.   
  10.     <c:if test="${errors.globalErrorCount > 0}">  
  11.         全局错误:<br/>  
  12.         <c:forEach items="${errors.globalErrors}" var="error">  
  13.             <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/>  
  14.             <c:if test="${not empty message}">  
  15.                 ${message}<br/>  
  16.             </c:if>  
  17.         </c:forEach>  
  18.     </c:if>  
  19. </spring:hasBindErrors>  

 

大家以后可以根据这个做通用的错误消息显示规则。比如我前端页面使用validationEngine显示错误消息,那么我可以定义一个tag来通用化错误消息的显示:showFieldError.tag。  

 

1.7、测试

输入如:http://localhost:9080/spring4/save?name=123 , 我们得到如下错误:

Java代码  收藏代码
  1. name------用户名必须是字母  
  2. name------用户名长度必须在520之间  
  3. password------密码不能为空  
  4. id------用户编号不能为空  

 

基本的集成就完成了。

 

如上测试有几个小问题:

1、错误消息顺序,大家可以看到name的错误消息顺序不是按照书写顺序的,即不确定;

2、我想显示如:用户名【zhangsan】必须在5到20之间;其中我们想动态显示:用户名、min,max;而不是写死了;

3、我想在修改的时候只验证用户名,其他的不验证怎么办。

接下来我们挨着试试吧。

 

2、分组验证及分组顺序

如果我们想在新增的情况验证id和name,而修改的情况验证name和password,怎么办? 那么就需要分组了。

首先定义分组接口:

Java代码  收藏代码
  1. public interface First {  
  2. }  
  3.   
  4. public interface Second {  
  5. }  

分组接口就是两个普通的接口,用于标识,类似于java.io.Serializable。

 

接着我们使用分组接口标识实体:

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.   
  3.     @NotNull(message = "{user.id.null}", groups = {First.class})  
  4.     private Long id;  
  5.   
  6.     @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {Second.class})  
  7.     @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})  
  8.     private String name;  
  9.   
  10.     @NotNull(message = "{user.password.null}", groups = {First.class, Second.class})  
  11.     private String password;  
  12. }  

 

验证时使用如:

Java代码  收藏代码
  1. @RequestMapping("/save")  
  2. public String save(@Validated({Second.class}) User user, BindingResult result) {  
  3.     if(result.hasErrors()) {  
  4.         return "error";  
  5.     }  
  6.     return "success";  
  7. }  

即通过@Validate注解标识要验证的分组;如果要验证两个的话,可以这样@Validated({First.class, Second.class})。

 

接下来我们来看看通过分组来指定顺序;还记得之前的错误消息吗? user.name会显示两个错误消息,而且顺序不确定;如果我们先验证一个消息;如果不通过再验证另一个怎么办?可以通过@GroupSequence指定分组验证顺序:

 

Java代码  收藏代码
  1. @GroupSequence({First.class, Second.class, User.class})  
  2. public class User implements Serializable {  
  3.     private Long id;  
  4.   
  5.     @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})  
  6.     @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})  
  7.     private String name;  
  8.       
  9.     private String password;  
  10. }  

通过@GroupSequence指定验证顺序:先验证First分组,如果有错误立即返回而不会验证Second分组,接着如果First分组验证通过了,那么才去验证Second分组,最后指定User.class表示那些没有分组的在最后。这样我们就可以实现按顺序验证分组了。

 

另一个比较常见的就是级联验证:

如:

Java代码  收藏代码
  1. public class User {  
  2.   
  3.     @Valid   
  4.     @ConvertGroup(from=First.class, to=Second.class)  
  5.     private Organization o;  
  6.   
  7. }  

 1、级联验证只要在相应的字段上加@Valid即可,会进行级联验证;@ConvertGroup的作用是当验证o的分组是First时,那么验证o的分组是Second,即分组验证的转换。

 

3、消息中使用EL表达式

假设我们需要显示如:用户名[NAME]长度必须在[MIN]到[MAX]之间,此处大家可以看到,我们不想把一些数据写死,如NAME、MIN、MAX;此时我们可以使用EL表达式。

 

如:

Java代码  收藏代码
  1. @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})  

错误消息:

Java代码  收藏代码
  1. user.name.length.illegal=用户名长度必须在{min}到{max}之间  

 

其中我们可以使用{验证注解的属性}得到这些值;如{min}得到@Length中的min值;其他的也是类似的。

 

到此,我们还是无法得到出错的那个输入值,如name=zhangsan。此时就需要EL表达式的支持,首先确定引入EL jar包且版本正确。然后使用如:

Java代码  收藏代码
  1. user.name.length.illegal=用户名[${validatedValue}]长度必须在520之间  

使用如EL表达式:${validatedValue}得到输入的值,如zhangsan。当然我们还可以使用如${min > 1 ? '大于1' : '小于等于1'},及在EL表达式中也能拿到如@Length的min等数据。

 

另外我们还可以拿到一个java.util.Formatter类型的formatter变量进行格式化:

Java代码  收藏代码
  1. ${formatter.format("%04d", min)}  

 

4、方法参数/返回值验证

这个可以参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》,概念是类似的,具体可以参考Bean Validation 文档。

 

5、自定义验证规则

有时候默认的规则可能还不够,有时候还需要自定义规则,比如屏蔽关键词验证是非常常见的一个功能,比如在发帖时帖子中不允许出现admin等关键词。

 

1、定义验证注解

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import javax.validation.Constraint;  
  4. import javax.validation.Payload;  
  5. import java.lang.annotation.Documented;  
  6. import java.lang.annotation.Retention;  
  7. import java.lang.annotation.Target;  
  8. import static java.lang.annotation.ElementType.*;  
  9. import static java.lang.annotation.RetentionPolicy.*;  
  10. /** 
  11.  * <p>User: Zhang Kaitao 
  12.  * <p>Date: 13-12-15 
  13.  * <p>Version: 1.0 
  14.  */  
  15.   
  16. @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })  
  17. @Retention(RUNTIME)  
  18. //指定验证器  
  19. @Constraint(validatedBy = ForbiddenValidator.class)  
  20. @Documented  
  21. public @interface Forbidden {  
  22.   
  23.     //默认错误消息  
  24.     String message() default "{forbidden.word}";  
  25.   
  26.     //分组  
  27.     Class<?>[] groups() default { };  
  28.   
  29.     //负载  
  30.     Class<? extends Payload>[] payload() default { };  
  31.   
  32.     //指定多个时使用  
  33.     @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })  
  34.     @Retention(RUNTIME)  
  35.     @Documented  
  36.     @interface List {  
  37.         Forbidden[] value();  
  38.     }  
  39. }  

 

2、 定义验证器

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;  
  4. import org.springframework.beans.factory.annotation.Autowired;  
  5. import org.springframework.context.ApplicationContext;  
  6. import org.springframework.util.StringUtils;  
  7.   
  8. import javax.validation.ConstraintValidator;  
  9. import javax.validation.ConstraintValidatorContext;  
  10. import java.io.Serializable;  
  11.   
  12. /** 
  13.  * <p>User: Zhang Kaitao 
  14.  * <p>Date: 13-12-15 
  15.  * <p>Version: 1.0 
  16.  */  
  17. public class ForbiddenValidator implements ConstraintValidator<Forbidden, String> {  
  18.   
  19.     private String[] forbiddenWords = {"admin"};  
  20.   
  21.     @Override  
  22.     public void initialize(Forbidden constraintAnnotation) {  
  23.         //初始化,得到注解数据  
  24.     }  
  25.   
  26.     @Override  
  27.     public boolean isValid(String value, ConstraintValidatorContext context) {  
  28.         if(StringUtils.isEmpty(value)) {  
  29.             return true;  
  30.         }  
  31.   
  32.         for(String word : forbiddenWords) {  
  33.             if(value.contains(word)) {  
  34.                 return false;//验证失败  
  35.             }  
  36.         }  
  37.         return true;  
  38.     }  
  39. }  

 验证器中可以使用spring的依赖注入,如注入:@Autowired  private ApplicationContext ctx; 

 

3、使用

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.     @Forbidden()  
  3.     private String name;  
  4. }  

 

4、当我们在提交name中含有admin的时候会输出错误消息:

Java代码  收藏代码
  1. forbidden.word=您输入的数据中有非法关键词  

 

问题来了,哪个词是非法的呢?bean validation 和 hibernate validator都没有提供相应的api提供这个数据,怎么办呢?通过跟踪代码,发现一种不是特别好的方法:我们可以覆盖org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl实现(即复制一份代码放到我们的src中),然后覆盖buildAnnotationParameterMap方法;

Java代码  收藏代码
  1. private Map<String, Object> buildAnnotationParameterMap(Annotation annotation) {  
  2.     ……  
  3.     //将Collections.unmodifiableMap( parameters );替换为如下语句  
  4.     return parameters;  
  5. }  

 即允许这个数据可以修改;然后在ForbiddenValidator中:

Java代码  收藏代码
  1. for(String word : forbiddenWords) {  
  2.     if(value.contains(word)) {  
  3.         ((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);  
  4.         return false;//验证失败  
  5.     }  
  6. }  

通过((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);添加自己的属性;放到attributes中的数据可以通过${} 获取。然后消息就可以变成:

Java代码  收藏代码
  1. forbidden.word=您输入的数据中有非法关键词【{word}】  

这种方式不是很友好,但是可以解决我们的问题。

 

典型的如密码、确认密码的场景,非常常用;如果没有这个功能我们需要自己写代码来完成;而且经常重复自己。接下来看看bean validation 1.1如何实现的。

 

6、类级别验证器

6.1、定义验证注解

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import javax.validation.Constraint;  
  4. import javax.validation.Payload;  
  5. import javax.validation.constraints.NotNull;  
  6. import java.lang.annotation.Documented;  
  7. import java.lang.annotation.Retention;  
  8. import java.lang.annotation.Target;  
  9. import static java.lang.annotation.ElementType.*;  
  10. import static java.lang.annotation.RetentionPolicy.*;  
  11. /** 
  12.  * <p>User: Zhang Kaitao 
  13.  * <p>Date: 13-12-15 
  14.  * <p>Version: 1.0 
  15.  */  
  16.   
  17. @Target({ TYPE, ANNOTATION_TYPE})  
  18. @Retention(RUNTIME)  
  19. //指定验证器  
  20. @Constraint(validatedBy = CheckPasswordValidator.class)  
  21. @Documented  
  22. public @interface CheckPassword {  
  23.   
  24.     //默认错误消息  
  25.     String message() default "";  
  26.   
  27.     //分组  
  28.     Class<?>[] groups() default { };  
  29.   
  30.     //负载  
  31.     Class<? extends Payload>[] payload() default { };  
  32.   
  33.     //指定多个时使用  
  34.     @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })  
  35.     @Retention(RUNTIME)  
  36.     @Documented  
  37.     @interface List {  
  38.         CheckPassword[] value();  
  39.     }  
  40. }  

6.2、 定义验证器

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import com.sishuok.spring4.entity.User;  
  4. import org.springframework.util.StringUtils;  
  5.   
  6. import javax.validation.ConstraintValidator;  
  7. import javax.validation.ConstraintValidatorContext;  
  8.   
  9. /** 
  10.  * <p>User: Zhang Kaitao 
  11.  * <p>Date: 13-12-15 
  12.  * <p>Version: 1.0 
  13.  */  
  14. public class CheckPasswordValidator implements ConstraintValidator<CheckPassword, User> {  
  15.   
  16.     @Override  
  17.     public void initialize(CheckPassword constraintAnnotation) {  
  18.     }  
  19.   
  20.     @Override  
  21.     public boolean isValid(User user, ConstraintValidatorContext context) {  
  22.         if(user == null) {  
  23.             return true;  
  24.         }  
  25.   
  26.         //没有填密码  
  27.         if(!StringUtils.hasText(user.getPassword())) {  
  28.             context.disableDefaultConstraintViolation();  
  29.             context.buildConstraintViolationWithTemplate("{password.null}")  
  30.                     .addPropertyNode("password")  
  31.                     .addConstraintViolation();  
  32.             return false;  
  33.         }  
  34.   
  35.         if(!StringUtils.hasText(user.getConfirmation())) {  
  36.             context.disableDefaultConstraintViolation();  
  37.             context.buildConstraintViolationWithTemplate("{password.confirmation.null}")  
  38.                     .addPropertyNode("confirmation")  
  39.                     .addConstraintViolation();  
  40.             return false;  
  41.         }  
  42.   
  43.         //两次密码不一样  
  44.         if (!user.getPassword().trim().equals(user.getConfirmation().trim())) {  
  45.             context.disableDefaultConstraintViolation();  
  46.             context.buildConstraintViolationWithTemplate("{password.confirmation.error}")  
  47.                     .addPropertyNode("confirmation")  
  48.                     .addConstraintViolation();  
  49.             return false;  
  50.         }  
  51.         return true;  
  52.     }  
  53. }  

其中我们通过disableDefaultConstraintViolation禁用默认的约束;然后通过buildConstraintViolationWithTemplate(消息模板)/addPropertyNode(所属属性)/addConstraintViolation定义我们自己的约束。

 

6.3、使用

Java代码  收藏代码
  1. @CheckPassword()  
  2. public class User implements Serializable {  
  3. }  

 放到类头上即可。

 

7、通过脚本验证

Java代码  收藏代码
  1. @ScriptAssert(script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")  
  2. public class User implements Serializable {  
  3. }  

通过脚本验证是非常简单而且强大的,lang指定脚本语言(请参考javax.script.ScriptEngineManager JSR-223),alias是在脚本验证中User对象的名字,但是大家会发现一个问题:错误消息怎么显示呢? 在springmvc 中会添加到全局错误消息中,这肯定不是我们想要的,我们改造下吧。

 

7.1、定义验证注解

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator;  
  4.   
  5. import java.lang.annotation.Documented;  
  6. import java.lang.annotation.Retention;  
  7. import java.lang.annotation.Target;  
  8. import javax.validation.Constraint;  
  9. import javax.validation.Payload;  
  10.   
  11. import static java.lang.annotation.ElementType.TYPE;  
  12. import static java.lang.annotation.RetentionPolicy.RUNTIME;  
  13.   
  14. @Target({ TYPE })  
  15. @Retention(RUNTIME)  
  16. @Constraint(validatedBy = {PropertyScriptAssertValidator.class})  
  17. @Documented  
  18. public @interface PropertyScriptAssert {  
  19.   
  20.     String message() default "{org.hibernate.validator.constraints.ScriptAssert.message}";  
  21.   
  22.     Class<?>[] groups() default { };  
  23.   
  24.     Class<? extends Payload>[] payload() default { };  
  25.   
  26.     String lang();  
  27.   
  28.     String script();  
  29.   
  30.     String alias() default "_this";  
  31.   
  32.     String property();  
  33.   
  34.     @Target({ TYPE })  
  35.     @Retention(RUNTIME)  
  36.     @Documented  
  37.     public @interface List {  
  38.         PropertyScriptAssert[] value();  
  39.     }  
  40. }  

和ScriptAssert没什么区别,只是多了个property用来指定出错后给实体的哪个属性。

 

7.2、验证器

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import javax.script.ScriptException;  
  4. import javax.validation.ConstraintDeclarationException;  
  5. import javax.validation.ConstraintValidator;  
  6. import javax.validation.ConstraintValidatorContext;  
  7.   
  8. import com.sishuok.spring4.validator.PropertyScriptAssert;  
  9. import org.hibernate.validator.constraints.ScriptAssert;  
  10. import org.hibernate.validator.internal.util.Contracts;  
  11. import org.hibernate.validator.internal.util.logging.Log;  
  12. import org.hibernate.validator.internal.util.logging.LoggerFactory;  
  13. import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluator;  
  14. import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluatorFactory;  
  15.   
  16. import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;  
  17.   
  18. public class PropertyScriptAssertValidator implements ConstraintValidator<PropertyScriptAssert, Object> {  
  19.   
  20.     private static final Log log = LoggerFactory.make();  
  21.   
  22.     private String script;  
  23.     private String languageName;  
  24.     private String alias;  
  25.     private String property;  
  26.     private String message;  
  27.   
  28.     public void initialize(PropertyScriptAssert constraintAnnotation) {  
  29.         validateParameters( constraintAnnotation );  
  30.   
  31.         this.script = constraintAnnotation.script();  
  32.         this.languageName = constraintAnnotation.lang();  
  33.         this.alias = constraintAnnotation.alias();  
  34.         this.property = constraintAnnotation.property();  
  35.         this.message = constraintAnnotation.message();  
  36.     }  
  37.   
  38.     public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {  
  39.   
  40.         Object evaluationResult;  
  41.         ScriptEvaluator scriptEvaluator;  
  42.   
  43.         try {  
  44.             ScriptEvaluatorFactory evaluatorFactory = ScriptEvaluatorFactory.getInstance();  
  45.             scriptEvaluator = evaluatorFactory.getScriptEvaluatorByLanguageName( languageName );  
  46.         }  
  47.         catch ( ScriptException e ) {  
  48.             throw new ConstraintDeclarationException( e );  
  49.         }  
  50.   
  51.         try {  
  52.             evaluationResult = scriptEvaluator.evaluate( script, value, alias );  
  53.         }  
  54.         catch ( ScriptException e ) {  
  55.             throw log.getErrorDuringScriptExecutionException( script, e );  
  56.         }  
  57.   
  58.         if ( evaluationResult == null ) {  
  59.             throw log.getScriptMustReturnTrueOrFalseException( script );  
  60.         }  
  61.         if ( !( evaluationResult instanceof Boolean ) ) {  
  62.             throw log.getScriptMustReturnTrueOrFalseException(  
  63.                     script,  
  64.                     evaluationResult,  
  65.                     evaluationResult.getClass().getCanonicalName()  
  66.             );  
  67.         }  
  68.   
  69.         if(Boolean.FALSE.equals(evaluationResult)) {  
  70.             constraintValidatorContext.disableDefaultConstraintViolation();  
  71.             constraintValidatorContext  
  72.                     .buildConstraintViolationWithTemplate(message)  
  73.                     .addPropertyNode(property)  
  74.                     .addConstraintViolation();  
  75.         }  
  76.   
  77.         return Boolean.TRUE.equals( evaluationResult );  
  78.     }  
  79.   
  80.     private void validateParameters(PropertyScriptAssert constraintAnnotation) {  
  81.         Contracts.assertNotEmpty( constraintAnnotation.script(), MESSAGES.parameterMustNotBeEmpty( "script" ) );  
  82.         Contracts.assertNotEmpty( constraintAnnotation.lang(), MESSAGES.parameterMustNotBeEmpty( "lang" ) );  
  83.         Contracts.assertNotEmpty( constraintAnnotation.alias(), MESSAGES.parameterMustNotBeEmpty( "alias" ) );  
  84.         Contracts.assertNotEmpty( constraintAnnotation.property(), MESSAGES.parameterMustNotBeEmpty( "property" ) );  
  85.         Contracts.assertNotEmpty( constraintAnnotation.message(), MESSAGES.parameterMustNotBeEmpty( "message" ) );  
  86.     }  
  87. }  

和之前的类级别验证器类似,就不多解释了,其他代码全部拷贝自org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator。

 

7.3、使用

Java代码  收藏代码
  1. @PropertyScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")  

和之前的区别就是多了个property,用来指定出错时给哪个字段。 这个相对之前的类级别验证器更通用一点。

 

8、cross-parameter,跨参数验证

直接看示例;

 

8.1、首先注册MethodValidationPostProcessor,起作用请参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》 

Java代码  收藏代码
  1. <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">  
  2.     <property name="validator" ref="validator"/>  
  3. </bean>  

 

8.2、Service 

Java代码  收藏代码
  1. @Validated  
  2. @Service  
  3. public class UserService {  
  4.   
  5.     @CrossParameter  
  6.     public void changePassword(String password, String confirmation) {  
  7.   
  8.     }  
  9. }  

通过@Validated注解UserService表示该类中有需要进行方法参数/返回值验证;   @CrossParameter注解方法表示要进行跨参数验证;即验证password和confirmation是否相等。

 

8.3、验证注解 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. //省略import  
  4.   
  5. @Constraint(validatedBy = CrossParameterValidator.class)  
  6. @Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })  
  7. @Retention(RUNTIME)  
  8. @Documented  
  9. public @interface CrossParameter {  
  10.   
  11.     String message() default "{password.confirmation.error}";  
  12.     Class<?>[] groups() default { };  
  13.     Class<? extends Payload>[] payload() default { };  
  14.   
  15. }  

 

8.4、验证器 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. //省略import  
  4.   
  5. @SupportedValidationTarget(ValidationTarget.PARAMETERS)  
  6. public class CrossParameterValidator implements ConstraintValidator<CrossParameter, Object[]> {  
  7.   
  8.     @Override  
  9.     public void initialize(CrossParameter constraintAnnotation) {  
  10.     }  
  11.   
  12.     @Override  
  13.     public boolean isValid(Object[] value, ConstraintValidatorContext context) {  
  14.         if(value == null || value.length != 2) {  
  15.             throw new IllegalArgumentException("must have two args");  
  16.         }  
  17.         if(value[0] == null || value[1] == null) {  
  18.             return true;  
  19.         }  
  20.         if(value[0].equals(value[1])) {  
  21.             return true;  
  22.         }  
  23.         return false;  
  24.     }  
  25. }  

其中@SupportedValidationTarget(ValidationTarget.PARAMETERS)表示验证参数; value将是参数列表。 

 

8.5、使用

Java代码  收藏代码
  1. @RequestMapping("/changePassword")  
  2. public String changePassword(  
  3.         @RequestParam("password") String password,  
  4.         @RequestParam("confirmation") String confirmation, Model model) {  
  5.     try {  
  6.         userService.changePassword(password, confirmation);  
  7.     } catch (ConstraintViolationException e) {  
  8.         for(ConstraintViolation violation : e.getConstraintViolations()) {  
  9.             System.out.println(violation.getMessage());  
  10.         }  
  11.     }  
  12.     return "success";  
  13. }  

调用userService.changePassword方法,如果验证失败将抛出ConstraintViolationException异常,然后得到ConstraintViolation,调用getMessage即可得到错误消息;然后到前台显示即可。

 

从以上来看,不如之前的使用方便,需要自己对错误消息进行处理。 下一节我们也写个脚本方式的跨参数验证器。

 

9、混合类级别验证器和跨参数验证器

9.1、验证注解

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. //省略import  
  4.   
  5. @Constraint(validatedBy = {  
  6.         CrossParameterScriptAssertClassValidator.class,  
  7.         CrossParameterScriptAssertParameterValidator.class  
  8. })  
  9. @Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE })  
  10. @Retention(RUNTIME)  
  11. @Documented  
  12. public @interface CrossParameterScriptAssert {  
  13.     String message() default "error";  
  14.     Class<?>[] groups() default { };  
  15.     Class<? extends Payload>[] payload() default { };  
  16.     String script();  
  17.     String lang();  
  18.     String alias() default "_this";  
  19.     String property() default "";  
  20.     ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;  
  21. }   

 

此处我们通过@Constraint指定了两个验证器,一个类级别的,一个跨参数的。validationAppliesTo指定为ConstraintTarget.IMPLICIT,表示隐式自动判断。

 

9.2、验证器

请下载源码查看

 

9.3、使用

9.3.1、类级别使用

Java代码  收藏代码
  1. @CrossParameterScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")  

指定property即可,其他和之前的一样。

9.3.2、跨参数验证

Java代码  收藏代码
  1. @CrossParameterScriptAssert(script = "args[0] == args[1]", lang = "javascript", alias = "args", message = "{password.confirmation.error}")  
  2. public void changePassword(String password, String confirmation) {  
  3.   
  4. }  

通过args[0]==args[1] 来判断是否相等。

 

这样,我们的验证注解就自动适应两种验证规则了。  

 

10、组合验证注解 

有时候,可能有好几个注解需要一起使用,此时就可以使用组合验证注解

Java代码  收藏代码
  1. @Target({ FIELD})  
  2. @Retention(RUNTIME)  
  3. @Documented  
  4. @NotNull(message = "{user.name.null}")  
  5. @Length(min = 5, max = 20, message = "{user.name.length.illegal}")  
  6. @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.length.illegal}")  
  7. @Constraint(validatedBy = { })  
  8. public @interface Composition {  
  9.     String message() default "";  
  10.     Class<?>[] groups() default { };  
  11.     Class<? extends Payload>[] payload() default { };  
  12. }  

这样我们验证时只需要:

Java代码  收藏代码
  1. @Composition()  
  2. private String name;  

简洁多了。 

 

11、本地化 

即根据不同的语言选择不同的错误消息显示。

1、本地化解析器

Java代码  收藏代码
  1. <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">  
  2.     <property name="cookieName" value="locale"/>  
  3.     <property name="cookieMaxAge" value="-1"/>  
  4.     <property name="defaultLocale" value="zh_CN"/>  
  5. </bean>  

此处使用cookie存储本地化信息,当然也可以选择其他的,如Session存储。

 

2、设置本地化信息的拦截器

Java代码  收藏代码
  1. <mvc:interceptors>  
  2.     <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">  
  3.         <property name="paramName" value="language"/>  
  4.     </bean>  
  5. </mvc:interceptors>  

即请求参数中通过language设置语言。

 

3、消息文件

 

4、 浏览器输入

http://localhost:9080/spring4/changePassword?password=1&confirmation=2&language=en_US

 

 

 

到此,我们已经完成大部分Bean Validation的功能实验了。对于如XML配置、编程式验证API的使用等对于我们使用SpringMVC这种web环境用处不大,所以就不多介绍了,有兴趣可以自己下载官方文档学习。

 

 

五、Spring4新特性——Groovy Bean定义DSL

 

Spring4支持使用Groovy DSL来进行Bean定义配置,其类似于XML,不过因为是Groovy DSL,可以实现任何复杂的语法配置,但是对于配置,我们需要那么复杂吗?本着学习的态度试用了下其Groovy DSL定义Bean,其主要缺点:

1、DSL语法规则不足,需要其后续维护;

2、编辑器的代码补全需要跟进,否则没有代码补全,写这个很痛苦;

3、出错提示不友好,排错难;

4、当前对于一些配置还是需要XML的支持,所以还不是100%的纯Groovy DSL;

5、目前对整个Spring生态支持还是不够的,比如Web,需要观望。

 

其优点就是其本质是Groovy脚本,所以可以做非常复杂的配置,如果以上问题能够解决,其也是一个不错的选择。在Groovy中的话使用这种配置感觉不会有什么问题,但是在纯Java开发环境下也是有它,给我的感觉是这个功能其目的是去推广它的groovy。比较怀疑它的动机。

 

接下来我们来看看Spring配置的发展:

Spring 2时代是XML风格配置  可以参考《跟我学Spring3》的前几章

Spring 3时代引入注解风格配置  可以参考《跟我学Spring3》的第12章 

Spring 4时代引入Groovy DSL风格来配置 后续讲解

 

一、对比

对于我来说,没有哪个好/坏,只有适用不适用;开发方便不方便。接下来我们来看一下各种类型的配置吧:

XML风格配置

Java代码  收藏代码
  1. <context:component-scan base-package="com.sishuok.spring4"/>  
  2. <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">  
  3.     <property name="validator" ref="validator"/>  
  4. </bean>  
  5. <mvc:annotation-driven validator="validator"/>  
  6.   
  7. <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">  
  8.     <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>  
  9.     <property name="validationMessageSource" ref="messageSource"/>  
  10. </bean>  

 

注解风格配置 

Java代码  收藏代码
  1. @Configuration  
  2. @EnableWebMvc  
  3. @ComponentScan(basePackages = "com.sishuok.spring4")  
  4. public class MvcConfiguration extends WebMvcConfigurationSupport {  
  5.     @Override  
  6.     protected Validator getValidator() {  
  7.         LocalValidatorFactoryBean localValidatorFactoryBean =  
  8.                 new LocalValidatorFactoryBean();  
  9.         localValidatorFactoryBean.setProviderClass(HibernateValidator.class);  
  10.         localValidatorFactoryBean.setValidationMessageSource(messageSource());  
  11.         return localValidatorFactoryBean;  
  12.     }  
  13. }  

 

Groovy DSL风格配置

Java代码  收藏代码
  1. import org.hibernate.validator.HibernateValidator  
  2. import org.springframework.context.support.ReloadableResourceBundleMessageSource  
  3. import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean  
  4.   
  5. beans {  
  6.     xmlns context: "http://www.springframework.org/schema/context"  
  7.     xmlns mvc: "http://www.springframework.org/schema/mvc"  
  8.   
  9.     context.'component-scan'('base-package'"com,sishuok.spring4")  
  10.     mvc.'annotation-driven'('validator'"validator")  
  11.   
  12.     validator(LocalValidatorFactoryBean) {  
  13.         providerClass = HibernateValidator.class  
  14.         validationMessageSource = ref("messageSource")  
  15.     }  
  16. }  

因为Spring4 webmvc没有提供用于Web环境的Groovy DSL实现的WebApplicationContext,所以为了在web环境使用,单独写了一个WebGenricGroovyApplicationContext,可以到源码中查找。

 

 

可以看到,它们之前差别不是特别大;以上只提取了部分配置,完整的配置可以参考我的github:spring4-showcase

 

对于注解风格的配置,如果在Servlet3容器中使用的话,可以借助WebApplicationInitializer实现无配置:

Java代码  收藏代码
  1. public class AppInitializer implements WebApplicationInitializer {  
  2.   
  3.     @Override  
  4.     public void onStartup(javax.servlet.ServletContext sc) throws ServletException {  
  5.   
  6. //        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();  
  7. //        rootContext.register(AppConfig.class);  
  8. //        sc.addListener(new ContextLoaderListener(rootContext));  
  9.   
  10.         //2、springmvc上下文  
  11.         AnnotationConfigWebApplicationContext springMvcContext = new AnnotationConfigWebApplicationContext();  
  12.         springMvcContext.register(MvcConfiguration.class);  
  13.   
  14.         //3、DispatcherServlet  
  15.         DispatcherServlet dispatcherServlet = new DispatcherServlet(springMvcContext);  
  16.         ServletRegistration.Dynamic dynamic = sc.addServlet("dispatcherServlet", dispatcherServlet);  
  17.         dynamic.setLoadOnStartup(1);  
  18.         dynamic.addMapping("/");  
  19.   
  20.         //4、CharacterEncodingFilter  
  21.         FilterRegistration filterRegistration =  
  22.                 sc.addFilter("characterEncodingFilter", CharacterEncodingFilter.class);  
  23.         filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false"/*");  
  24.   
  25.   
  26.     }  
  27. }  

 

到底好还是不好,需要根据自己项目大小等一些因素来衡量。对于Servlet3可以参考我github的示例:servlet3-showcase  

 

对于Groovy风格配置,如果语法足够丰富、Spring内部支持完善,且编辑器支持也非常好的话,也是不错的选择。

 

 

二、Groovy Bean定义

接下来我们来看下groovy DSL的具体使用吧:

1、安装环境

Java代码  收藏代码
  1. <dependency>  
  2.     <groupId>org.codehaus.groovy</groupId>  
  3.     <artifactId>groovy-all</artifactId>  
  4.     <version>${groovy.version}</version>  
  5. </dependency>  

我使用的groovy版本是2.2.1

 

2、相关组件类

此处使用Spring Framework官网的hello world,可以前往http://projects.spring.io/spring-framework/ 主页查看 

 

3、Groovy Bean定义配置文件

Java代码  收藏代码
  1. import com.sishuok.spring4.xml.MessageServiceImpl  
  2. import com.sishuok.spring4.xml.MessagePrinter  
  3.   
  4. beans {  
  5.     messageService(MessageServiceImpl) {//名字(类型)   
  6.         message = "hello"  //注入的属性  
  7.     }  
  8.   
  9.     messagePrinter(MessagePrinter, messageService) //名字(类型,构造器参数列表)  
  10.   
  11. }  

从此处可以看到 如果仅仅是简单的Bean定义,确实比XML简洁。

 

 

4、测试

如果不测试环境可以这样测试:

Java代码  收藏代码
  1. public class XmlGroovyBeanDefinitionTest1 {  
  2.     @Test  
  3.     public void test() {  
  4.         ApplicationContext ctx = new GenericGroovyApplicationContext("classpath:spring-config-xml.groovy");  
  5.         MessagePrinter messagePrinter = (MessagePrinter) ctx.getBean("messagePrinter");  
  6.         messagePrinter.printMessage();  
  7.     }  
  8. }  

使用GenericGroovyApplicationContext加载groovy配置文件。 

 

 

如果想集成到Spring Test中,可以这样:

Java代码  收藏代码
  1. @RunWith(SpringJUnit4ClassRunner.class)  
  2. @ContextConfiguration(locations = "classpath:spring-config-xml.groovy", loader = GenericGroovyContextLoader.class)  
  3. public class XmlGroovyBeanDefinitionTest2 {  
  4.   
  5.     @Autowired  
  6.     private MessagePrinter messagePrinter;  
  7.   
  8.     @Test  
  9.     public void test() {  
  10.         messagePrinter.printMessage();  
  11.     }  
  12. }  

此处需要定义我们自己的bean loader,即从groovy配置文件加载:

Java代码  收藏代码
  1. public class GenericGroovyContextLoader extends AbstractGenericContextLoader {  
  2.   
  3.     @Override  
  4.     protected String getResourceSuffix() {  
  5.         throw new UnsupportedOperationException(  
  6.                 "GenericGroovyContextLoader does not support the getResourceSuffix() method");  
  7.     }  
  8.     @Override  
  9.     protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) {  
  10.         return new GroovyBeanDefinitionReader(context);  
  11.     }  
  12. }  

使用GroovyBeanDefinitionReader来加载groovy配置文件。  

 

到此基本的使用就结束了,还算是比较简洁,但是我们已经注意到了,在纯Java环境做测试还是比较麻烦的。 比如没有给我们写好相关的测试支撑类。另外大家可以前往Spring的github看看在groovy中的单元测试:GroovyBeanDefinitionReaderTests.groovy

 

再看一下我们使用注解方式呢:

Java代码  收藏代码
  1. @Component  
  2. public class MessageServiceImpl implements MessageService {  
  3.     @Autowired  
  4.     @Qualifier("message")  
  5.     private String message;  
  6.     ……  
  7. }  
 
Java代码  收藏代码
  1. @Component  
  2. public class MessagePrinter {  
  3.     private MessageService messageService;  
  4.     @Autowired  
  5.     public MessagePrinter(MessageService messageService) {  
  6.         this.messageService = messageService;  
  7.     }  
  8. ……  
  9. }  

 

此处省略无关代码,需要的话直接去github查看 。点击前往 

 

Groovy配置文件:

 

Java代码  收藏代码
  1. beans {  
  2.     xmlns context: "http://www.springframework.org/schema/context"    //导入命名空间  
  3.   
  4.     context.'component-scan'('base-package'"com.sishuok.spring4") {  
  5.         'exclude-filter'('type'"aspectj"'expression'"com.sishuok.spring4.xml.*")  
  6.     }  
  7.   
  8.     message(String, "hello") {}  
  9.   
  10. }  
在该配置文件中支持导入xml命名空间, 其中context.'component-scan'部分等价于XML中的:
Java代码  收藏代码
  1. <context:component-scan base-package="com.sishuok.spring4">  
  2.     <context:exclude-filter type="aspectj" expression="com.sishuok.spring4.xml.*"/>  
  3. </context:component-scan>              
 从这里可以看出,其还没能完全从XML风格配置中走出来,不是纯Groovy DSL。

 

 

测试方式和之前的一样就不重复了,可以查看XmlGroovyBeanDefinitionTest2.java

 

三、Groovy Bean定义 DSL语法

到目前为止,基本的helloworld就搞定了;接下来看看Groovy DSL都支持哪些配置吧:

创建Bean

 

构造器

Java代码  收藏代码
  1. validator(LocalValidatorFactoryBean) { //名字(类型)  
  2.     providerClass = HibernateValidator.class  //属性=值  
  3.     validationMessageSource = ref("messageSource"//属性 = 引用,当然也支持如 validationMessageSource=messageSource 但是这种方式缺点是messageSource必须在validator之前声明  
  4. }   

静态工厂方法

Java代码  收藏代码
  1. def bean = factory(StaticFactory) {  
  2.     prop = 1  
  3. }  
  4. bean.factoryMethod = "getInstance"  
或者
Java代码  收藏代码
  1. bean(StaticFactory) { bean ->  
  2.     bean.factoryMethod = "getInstance"  
  3.     prop = 1  
  4. }  
 
实例工厂方法
Java代码  收藏代码
  1. beanFactory(Factory)  
  2. bean(beanFactory : "newInstance""args") {  
  3.     prop = 1  
  4. }  
或者
Java代码  收藏代码
  1. beanFactory(Factory)  
  2. bean("bean"){bean ->  
  3.     bean.factoryBean="beanFactory"  
  4.     bean.factoryMethod="newInstance"  
  5.     prop = 1  
  6. }  

 

依赖注入

 

属性注入

Java代码  收藏代码
  1. beanName(BeanClass) { //名字(类型)  
  2.     str = "123" // 常量直接注入  
  3.     bean = ref("bean"//属性 = 引用 ref("bean", true) 这样的话是引用父容器的  
  4.     beans = [bean1, bean2] //数组/集合  
  5.     props = [key1:"value1", key2:"value2"// Properties / Map  
  6. }  

 

构造器注入 

Java代码  收藏代码
  1. bean(Bean, "args1""args2")   

 

静态工厂注入/实例工厂注入,请参考创建bean部分

 

 

匿名内部Bean

Java代码  收藏代码
  1. outer(OuterBean) {  
  2.    prop = 1  
  3.    inner =  { InnerBean bean ->  //匿名内部Bean  
  4.                           prop =2  
  5.             }  
  6. }  
Java代码  收藏代码
  1. outer(OuterBean) {  
  2.    prop = 1  
  3.    inner =  { bean ->  //匿名内部Bean 通过实例工厂方法创建  
  4.                           bean.factoryBean = "innerBean"  
  5.                           bean.factoryMethod = "create"  
  6.                           prop = 2  
  7.             }  
  8. }  

单例/非单例/作用域

Java代码  收藏代码
  1. singletonBean(Bean1) { bean ->  
  2.     bean.singleton = true  
  3. }  
  4. nonSingletonBean(Bean1) { bean ->  
  5.     bean.singleton = false  
  6. }  
  7. prototypeBean(Bean1) { bean ->  
  8.     bean.scope = "prototype"  
  9. }  

 

其中bean可以理解为xml中的<bean> 标签,即bean定义。

 

 

父子Bean

Java代码  收藏代码
  1. parent(Bean1){ bean ->   
  2.     bean.'abstract' = true //抽象的  
  3.     prop = 123  
  4. }  
  5. child { bean ->  
  6.     bean.parent = parent //指定父bean  
  7. }  
 

 

命名空间

Java代码  收藏代码
  1. xmlns aop:"http://www.springframework.org/schema/aop"  
  2. myAspect(MyAspect)  
  3. aop {  
  4.     config("proxy-target-class":true) {  
  5.         aspect( id:"test",ref:"myAspect" ) {  
  6.             before method:"before", pointcut: "execution(void com.sishuok.spring4..*.*(..))"  
  7.         }  
  8.     }  
  9. }  
以上是AOP的,可以自己推到其他相关的配置; 

 

Java代码  收藏代码
  1. xmlns context: "http://www.springframework.org/schema/context"     
  2. context.'component-scan'('base-package'"com.sishuok.spring4") {  
  3.     'exclude-filter'('type'"aspectj"'expression'"com.sishuok.spring4.xml.*")  
  4. }  
以上是component-scan,之前介绍过了。

  

Java代码  收藏代码
  1. xmlns aop:"http://www.springframework.org/schema/aop"  
  2. scopedList(ArrayList) { bean ->  
  3.     bean.scope = "haha"  
  4.     aop.'scoped-proxy'()    
  5. }  
 等价于
Java代码  收藏代码
  1. <bean id="scopedList" class="java.util.ArrayList" scope="haha">  
  2.     <aop:scoped-proxy/>  
  3. </bean>  

 

Java代码  收藏代码
  1. xmlns util:"http://www.springframework.org/schema/util"  
  2. util.list(id : 'list') {  
  3.     value 1  
  4.     value 2  
  5. }  
等价于XML:
Java代码  收藏代码
  1. <util:list id="list">  
  2.     <value>1</value>  
  3.     <value>2</value>  
  4. </util:list>  

 

Java代码  收藏代码
  1. xmlns util:"http://www.springframework.org/schema/util"  
  2. util.map(id : 'map') {  
  3.     entry(key : 1, value :1)  
  4.     entry('key-ref' : "messageService"'value-ref' : "messageService")  
  5. }  
 等价于
Java代码  收藏代码
  1. <util:map id="map">  
  2.     <entry key="1" value="1"/>  
  3.     <entry key-ref="messageService" value-ref="messageService"/>  
  4. </util:map>  

引入其他配置文件

Java代码  收藏代码
  1. importBeans "classpath:org/springframework/context/groovy/test.xml"  
当然也能引入XML的。 

 

对于DSL新的更新大家可以关注:GroovyBeanDefinitionReaderTests.groovy,本文也是根据其编写的。

 

再来看看groovy bean定义的另一个好处:

我们可以直接在groovy bean定义文件中声明类,然后使用

Java代码  收藏代码
  1. @Controller  
  2. def class GroovyController {  
  3.     @RequestMapping("/groovy")  
  4.     @ResponseBody  
  5.     public String hello() {  
  6.         return "hello";  
  7.     }  
  8. }  
  9.   
  10. beans {  
  11.   
  12.     groovyController(GroovyController)  
  13.   
  14. }  

 

 另一种Spring很早就支持的方式是引入外部groovy文件,如:

Java代码  收藏代码
  1. xmlns lang: "http://www.springframework.org/schema/lang"  
  2. lang.'groovy'(id: 'groovyController2''script-source''classpath:com/sishuok/spring4/controller/GroovyController2.groovy')  

使用其lang命名空间引入外部脚本文件。

 

 

到此,Groovy Bean定义DSL就介绍完了,其没有什么特别之处,只是换了种写法而已,我认为目前试试即可,还不能用到真实环境。

 

 

示例代码:

https://github.com/zhangkaitao/spring4-showcase

https://github.com/zhangkaitao/servlet3-showcase

 

六、Spring4新特性——更好的Java泛型操作API :

随着泛型用的越来越多,获取泛型实际类型信息的需求也会出现,如果用原生API,需要很多步操作才能获取到泛型,比如:

Java代码  收藏代码
  1. ParameterizedType parameterizedType =   
  2.     (ParameterizedType) ABService.class.getGenericInterfaces()[0];  
  3. Type genericType = parameterizedType.getActualTypeArguments()[1];  

 

Spring提供的ResolvableType API,提供了更加简单易用的泛型操作支持,如:

Java代码  收藏代码
  1. ResolvableType resolvableType1 = ResolvableType.forClass(ABService.class);  
  2. resolvableType1.as(Service.class).getGeneric(1).resolve()  

对于获取更复杂的泛型操作ResolvableType更加简单。

 

假设我们的API是:

Java代码  收藏代码
  1. public interface Service<N, M> {  
  2. }  
  3.   
  4. @org.springframework.stereotype.Service  
  5. public class ABService implements Service<A, B> {  
  6. }  
  7.   
  8. @org.springframework.stereotype.Service  
  9. public class CDService implements Service<C, D> {  
  10. }  

如上泛型类非常简单。 

 

1、得到类型的泛型信息

Java代码  收藏代码
  1. ResolvableType resolvableType1 = ResolvableType.forClass(ABService.class);  

通过如上API,可以得到类型的ResolvableType,如果类型被Spring AOP进行了CGLIB代理,请使用ClassUtils.getUserClass(ABService.class)得到原始类型。

 

可以通过如下得到泛型参数的第1个位置(从0开始)的类型信息

Java代码  收藏代码
  1. resolvableType1.getInterfaces()[0].getGeneric(1).resolve()  

因为我们泛型信息放在 Service<A, B> 上,所以需要resolvableType1.getInterfaces()[0]得到;

 

通过getGeneric(泛型参数索引)得到某个位置的泛型;

resolve()把实际泛型参数解析出来

 

2、得到字段级别的泛型信息

假设我们的字段如下:

Java代码  收藏代码
  1. @Autowired  
  2.  private Service<A, B> abService;  
  3.  @Autowired  
  4.  private Service<C, D> cdService;  
  5.   
  6.  private List<List<String>> list;  
  7.   
  8.  private Map<String, Map<String, Integer>> map;  
  9.   
  10.  private List<String>[] array;  

 

通过如下API可以得到字段级别的ResolvableType

Java代码  收藏代码
  1. ResolvableType resolvableType2 =  
  2.                 ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class"cdService"));  

 

然后通过如下API得到Service<C, D>的第0个位置上的泛型实参类型,即C

Java代码  收藏代码
  1. resolvableType2.getGeneric(0).resolve()  

 

比如 List<List<String>> list;是一种嵌套的泛型用例,我们可以通过如下操作获取String类型:

Java代码  收藏代码
  1. ResolvableType resolvableType3 =  
  2.                 ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class"list"));  
  3. resolvableType3.getGeneric(0).getGeneric(0).resolve();  

 

更简单的写法

Java代码  收藏代码
  1. resolvableType3.getGeneric(00).resolve(); //List<List<String>> 即String   

 

比如Map<String, Map<String, Integer>> map;我们想得到Integer,可以使用:

Java代码  收藏代码
  1. ResolvableType resolvableType4 =  
  2.                 ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class"map"));  
  3. resolvableType4.getGeneric(1).getGeneric(1).resolve();  

更简单的写法  

Java代码  收藏代码
  1. resolvableType4.getGeneric(11).resolve()  

 

3、得到方法返回值的泛型信息

假设我们的方法如下:

Java代码  收藏代码
  1. private HashMap<String, List<String>> method() {  
  2.     return null;  
  3. }  

 

得到Map中的List中的String泛型实参:

Java代码  收藏代码
  1. ResolvableType resolvableType5 =  
  2.                 ResolvableType.forMethodReturnType(ReflectionUtils.findMethod(GenricInjectTest.class"method"));  
  3. resolvableType5.getGeneric(10).resolve();  

 

4、得到构造器参数的泛型信息

假设我们的构造器如下:

Java代码  收藏代码
  1. public Const(List<List<String>> list, Map<String, Map<String, Integer>> map) {  
  2. }  

我们可以通过如下方式得到第1个参数( Map<String, Map<String, Integer>>)中的Integer:

Java代码  收藏代码
  1. ResolvableType resolvableType6 =  
  2.                 ResolvableType.forConstructorParameter(ClassUtils.getConstructorIfAvailable(Const.class, List.class, Map.class), 1);  
  3. resolvableType6.getGeneric(10).resolve();  

 

5、得到数组组件类型的泛型信息

如对于private List<String>[] array; 可以通过如下方式获取List的泛型实参String:

Java代码  收藏代码
  1. ResolvableType resolvableType7 =  
  2.                 ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class"array"));  
  3. resolvableType7.isArray();//判断是否是数组  
  4. resolvableType7.getComponentType().getGeneric(0).resolve();  

 

6、自定义泛型类型

Java代码  收藏代码
  1. ResolvableType resolvableType8 = ResolvableType.forClassWithGenerics(List.class, String.class);  
  2.         ResolvableType resolvableType9 = ResolvableType.forArrayComponent(resolvableType8);  
  3. resolvableType9.getComponentType().getGeneric(0).resolve();  

ResolvableType.forClassWithGenerics(List.class, String.class)相当于创建一个List<String>类型;

ResolvableType.forArrayComponent(resolvableType8);:相当于创建一个List<String>[]数组;

resolvableType9.getComponentType().getGeneric(0).resolve():得到相应的泛型信息;

 

7、泛型等价比较:

Java代码  收藏代码
  1. resolvableType7.isAssignableFrom(resolvableType9)  

 

如下创建一个List<Integer>[]数组,与之前的List<String>[]数组比较,将返回false。

Java代码  收藏代码
  1. ResolvableType resolvableType10 = ResolvableType.forClassWithGenerics(List.class, Integer.class);  
  2. ResolvableType resolvableType11= ResolvableType.forArrayComponent(resolvableType10);  
  3. resolvableType11.getComponentType().getGeneric(0).resolve();  
  4. resolvableType7.isAssignableFrom(resolvableType11);  

 

从如上操作可以看出其泛型操作功能十分完善,尤其在嵌套的泛型信息获取上相当简洁。目前整个Spring4环境都使用这个API来操作泛型信息。

 

如之前说的泛型注入:Spring4新特性——泛型限定式依赖注入,通过在依赖注入时使用如下类实现:

GenericTypeAwareAutowireCandidateResolver

QualifierAnnotationAutowireCandidateResolver

ContextAnnotationAutowireCandidateResolver

 

还有如Spring的核心BeanWrapperImpl,以及整个Spring/SpringWevMVC的泛型操作都是替换为这个API了:GenericCollectionTypeResolver和GenericTypeResolver都直接委托给ResolvableType这个API。

 

所以大家以后对泛型操作可以全部使用这个API了,非常好用。测试用例请参考GenricInjectTest.java

七、Spring4新特性——JSR310日期API的支持

JSR-310规范提供一个新的和改进的Java日期与时间API。从2007投票到2013年11月发布公共Review版本已经好多年了,会在Java8中包含,可以下载OpenJDK早期发布版本试用(Win XP不支持):

 

https://jdk8.java.net/download.html

 

JSR 310规范领导者Stephen Colebourne就是joda-time作者,其主要思想也是借鉴了joda-time,而不是直接把joda-time移植到Java平台中,API是类似的,但做了改进,具体的改进请参考其2009年的一篇文章和InfoQ对他的采访:

http://blog.joda.org/2009/11/why-jsr-310-isn-joda-time_4941.html 

http://www.infoq.com/cn/news/2010/05/jsr-310 

http://blog.joda.org/2010/12/what-about-jsr-310_153.html

 

规范请前往如下地址下载:

https://jcp.org/en/jsr/detail?id=310

 

JSR310 日期与时间规范主要API如下:

Clock

时钟,类似于钟表的概念,提供了如系统时钟、固定时钟、特定时区的时钟

Java代码  收藏代码
  1. //时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。  
  2. Clock c1 = Clock.systemUTC(); //系统默认UTC时钟(当前瞬时时间 System.currentTimeMillis())  
  3. System.out.println(c1.millis()); //每次调用将返回当前瞬时时间(UTC)  
  4.   
  5. Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)  
  6.   
  7. Clock c31 = Clock.system(ZoneId.of("Europe/Paris")); //巴黎时区  
  8. System.out.println(c31.millis()); //每次调用将返回当前瞬时时间(UTC)  
  9.   
  10. Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));//上海时区  
  11. System.out.println(c32.millis());//每次调用将返回当前瞬时时间(UTC)  
  12.   
  13. Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));//固定上海时区时钟  
  14. System.out.println(c4.millis());  
  15. Thread.sleep(1000);  
  16. System.out.println(c4.millis()); //不变 即时钟时钟在那一个点不动  
  17.   
  18. Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟  
  19. System.out.println(c1.millis());  
  20. System.out.println(c5.millis());  

 

 

Instant

瞬时时间,等价于以前的System.currentTimeMillis()

Java代码  收藏代码
  1. //瞬时时间 相当于以前的System.currentTimeMillis()  
  2. Instant instant1 = Instant.now();  
  3. System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于1970-01-01 00:00:00 UTC的一个时间  
  4. System.out.println(instant1.toEpochMilli()); //精确到毫秒  
  5.   
  6. Clock clock1 = Clock.systemUTC(); //获取系统UTC默认时钟  
  7. Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间  
  8. System.out.println(instant2.toEpochMilli());  
  9.   
  10. Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟  
  11. Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间  
  12. System.out.println(instant3.toEpochMilli());//equals instant1  

 

LocalDateTime、LocalDate、LocalTime 

提供了对java.util.Date的替代,另外还提供了新的DateTimeFormatter用于对格式化/解析的支持

Java代码  收藏代码
  1. //使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()默认时区  
  2. LocalDateTime now = LocalDateTime.now();  
  3. System.out.println(now);  
  4.   
  5. //自定义时区  
  6. LocalDateTime now2= LocalDateTime.now(ZoneId.of("Europe/Paris"));  
  7. System.out.println(now2);//会以相应的时区显示日期  
  8.   
  9. //自定义时钟  
  10. Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));  
  11. LocalDateTime now3= LocalDateTime.now(clock);  
  12. System.out.println(now3);//会以相应的时区显示日期  
  13.   
  14. //不需要写什么相对时间 如java.util.Date 年是相对于1900 月是从0开始  
  15. //2013-12-31 23:59  
  16. LocalDateTime d1 = LocalDateTime.of(201312312359);  
  17.   
  18. //年月日 时分秒 纳秒  
  19. LocalDateTime d2 = LocalDateTime.of(201312312359,5911);  
  20.   
  21. //使用瞬时时间 + 时区  
  22. Instant instant = Instant.now();  
  23. LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());  
  24. System.out.println(d3);  
  25.   
  26. //解析String--->LocalDateTime  
  27. LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");  
  28. System.out.println(d4);  
  29.   
  30. LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");//999毫秒 等价于999000000纳秒  
  31. System.out.println(d5);  
  32.   
  33. //使用DateTimeFormatter API 解析 和 格式化  
  34. DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");  
  35. LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);  
  36. System.out.println(formatter.format(d6));  
  37.   
  38.   
  39. //时间获取  
  40. System.out.println(d6.getYear());  
  41. System.out.println(d6.getMonth());  
  42. System.out.println(d6.getDayOfYear());  
  43. System.out.println(d6.getDayOfMonth());  
  44. System.out.println(d6.getDayOfWeek());  
  45. System.out.println(d6.getHour());  
  46. System.out.println(d6.getMinute());  
  47. System.out.println(d6.getSecond());  
  48. System.out.println(d6.getNano());  
  49. //时间增减  
  50. LocalDateTime d7 = d6.minusDays(1);  
  51. LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);  
  52.   
  53. //LocalDate 即年月日 无时分秒  
  54. //LocalTime即时分秒 无年月日  
  55. //API和LocalDateTime类似就不演示了  

 

ZonedDateTime

带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义);API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])  

Java代码  收藏代码
  1. //即带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义)。  
  2. //API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])  
  3. ZonedDateTime now = ZonedDateTime.now();  
  4. System.out.println(now);  
  5.   
  6. ZonedDateTime now2= ZonedDateTime.now(ZoneId.of("Europe/Paris"));  
  7. System.out.println(now2);  
  8.   
  9. //其他的用法也是类似的 就不介绍了  
  10.   
  11. ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");  
  12. System.out.println(z1);  

 

 

Duration

表示两个瞬时时间的时间段 

 

Java代码  收藏代码
  1. //表示两个瞬时时间的时间段  
  2. Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());  
  3. //得到相应的时差  
  4. System.out.println(d1.toDays());  
  5. System.out.println(d1.toHours());  
  6. System.out.println(d1.toMinutes());  
  7. System.out.println(d1.toMillis());  
  8. System.out.println(d1.toNanos());  
  9.   
  10. //1天时差 类似的还有如ofHours()  
  11. Duration d2 = Duration.ofDays(1);  
  12. System.out.println(d2.toDays());  

 

 

Chronology

用于对年历系统的支持,是java.util.Calendar的替代者

Java代码  收藏代码
  1. //提供对java.util.Calendar的替换,提供对年历系统的支持  
  2. Chronology c  = HijrahChronology.INSTANCE;  
  3. ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());  
  4. System.out.println(d);  

  

其他

如果提供了年、年月、月日、周期的API支持

Java代码  收藏代码
  1. Year year = Year.now();  
  2. YearMonth yearMonth = YearMonth.now();  
  3. MonthDay monthDay = MonthDay.now();  
  4.   
  5. System.out.println(year);//年  
  6. System.out.println(yearMonth); //年-月  
  7. System.out.println(monthDay); // 月-日  
  8.   
  9. //周期,如表示10天前  3年5个月钱  
  10. Period period1 = Period.ofDays(10);  
  11. System.out.println(period1);  
  12. Period period2 = Period.of(350);  
  13. System.out.println(period2);  

 

代码示例请参考:TimeTest.java

 

从以上来看,JSR310提供了更好、更强大的、更易使用的API。另外有三篇对joda-time和jsr310 使用的介绍文章:

http://www.codedata.com.tw/java/jodatime-jsr310-1-date-calendar/

http://www.codedata.com.tw/java/jodatime-jsr310-2-time-abc/

http://www.codedata.com.tw/java/jodatime-jsr310-3-using-jodatime/

 

spring4提供了对jsr310的支持,只要能发现如java.time.LocalDate,DefaultFormattingConversionService就会自动注册对jsr310的支持;对于ConversionService请参考:

SpringMVC数据格式化——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC 

 

我们只需要在实体/Bean上使用DateTimeFormat注解:

Java代码  收藏代码
  1. @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
  2. private LocalDateTime dateTime;  
  3.   
  4. @DateTimeFormat(pattern = "yyyy-MM-dd")  
  5. private LocalDate date;  
  6.   
  7. @DateTimeFormat(pattern = "HH:mm:ss")  
  8. private LocalTime time;  

比如我们在springmvc中:

Java代码  收藏代码
  1. @RequestMapping("/test")  
  2. public String test(@ModelAttribute("entity") Entity entity) {  
  3.     return "test";  
  4. }  

当前端页面请求:

localhost:9080/spring4/test?dateTime=2013-11-11 11:11:11&date=2013-11-11&time=12:12:12

会自动进行类型转换。

 

另外spring4也提供了对TimeZone的支持;比如在springmvc中注册了LocaleContextResolver相应实现的话(如CookieLocaleResolver),我们就可以使用如下两种方式得到相应的TimeZone:

RequestContextUtils.getTimeZone(request)

LocaleContextHolder.getTimeZone()

 

不过目前的缺点是不能像Local那样自动的根据当前请求得到相应的TimeZone,如果需要这种功能需要覆盖相应的如CookieLocaleResolver中的如下方法来得到:

Java代码  收藏代码
  1. protected TimeZone determineDefaultTimeZone(HttpServletRequest request) {  
  2.     return getDefaultTimeZone();  
  3. }  

 

另外还提供了DateTimeContextHolder,其用于线程绑定DateTimeContext;而DateTimeContext提供了如:Chronology、ZoneId、DateTimeFormatter等上下文数据,如果需要这种上下文信息的话,可以使用这个API进行绑定。比如在进行日期格式化时,就会去查找相应的DateTimeFormatter,因此如果想自定义相应的格式化格式,那么使用DateTimeContextHolder绑定即可。

 

源代码请参考github项目:spring4-datetime-jsr310 

 

spring4只是简单的对jsr310提供了相应的支持,没有太多的增强。

八、Spring4新特性——注解、脚本、任务、MVC等其他特性改进 :

一、注解方面的改进

spring4对注解API和ApplicationContext获取注解Bean做了一点改进。

获取注解的注解,如@Service是被@Compent注解的注解,可以通过如下方式获取@Componet注解实例:

Java代码  收藏代码
  1. Annotation service = AnnotationUtils.findAnnotation(ABService.class, org.springframework.stereotype.Service.class);  
  2. Annotation component = AnnotationUtils.getAnnotation(service, org.springframework.stereotype.Component.class);  

 

获取重复注解:

比如在使用hibernate validation时,我们想在一个方法上加相同的注解多个,需要使用如下方式:

Java代码  收藏代码
  1. @Length.List(  
  2.         value = {  
  3.                 @Length(min = 1, max = 2, groups = A.class),  
  4.                 @Length(min = 3, max = 4, groups = B.class)  
  5.         }  
  6. )  
  7. public void test() {  

可以通过如下方式获取@Length:

Java代码  收藏代码
  1. Method method = ClassUtils.getMethod(AnnotationUtilsTest.class"test");  
  2. Set<Length> set = AnnotationUtils.getRepeatableAnnotation(method, Length.List.class, Length.class);  

 

当然,如果你使用Java8,那么本身就支持重复注解,比如spring的任务调度注解,

Java代码  收藏代码
  1. @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. @Repeatable(Schedules.class)  
  5. public @interface Scheduled {   
Java代码  收藏代码
  1. @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. public @interface Schedules {  
  5.   
  6.     Scheduled[] value();  
  7.   
  8. }  

 

这样的话,我们可以直接同时注解相同的多个注解:

Java代码  收藏代码
  1. @Scheduled(cron = "123")  
  2. @Scheduled(cron = "234")  
  3. public void test     

但是获取的时候还是需要使用如下方式:

Java代码  收藏代码
  1. AnnotationUtils.getRepeatableAnnotation(ClassUtils.getMethod(TimeTest.class"test"), Schedules.class, Scheduled.class)  

 

ApplicationContext和BeanFactory提供了直接通过注解获取Bean的方法:

Java代码  收藏代码
  1. @Test  
  2. public void test() {  
  3.     AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();  
  4.     ctx.register(GenericConfig.class);  
  5.     ctx.refresh();  
  6.   
  7.     Map<String, Object> beans = ctx.getBeansWithAnnotation(org.springframework.stereotype.Service.class);  
  8.     System.out.println(beans);  
  9. }  

这样可以实现一些特殊功能。

 

另外和提供了一个AnnotatedElementUtils用于简化java.lang.reflect.AnnotatedElement的操作,具体请参考其javadoc。   

 

二、脚本的支持 

spring4也提供了类似于javax.script的简单封装,用于支持一些脚本语言,核心接口是:

Java代码  收藏代码
  1. public interface ScriptEvaluator {  
  2.     Object evaluate(ScriptSource script) throws ScriptCompilationException;  
  3.     Object evaluate(ScriptSource script, Map<String, Object> arguments) throws ScriptCompilationException;  
  4. }  

 

比如我们使用groovy脚本的话,可以这样:

Java代码  收藏代码
  1. @Test  
  2. public void test() throws ExecutionException, InterruptedException {  
  3.     ScriptEvaluator scriptEvaluator = new GroovyScriptEvaluator();  
  4.   
  5.     //ResourceScriptSource 外部的  
  6.     ScriptSource source = new StaticScriptSource("i+j");  
  7.     Map<String, Object> args = new HashMap<>();  
  8.     args.put("i"1);  
  9.     args.put("j"2);  
  10.     System.out.println(scriptEvaluator.evaluate(source, args));  
  11. }  

没什么很特别的地方。另外还提供了BeanShell(BshScriptEvaluator)和javax.script(StandardScriptEvaluator)的简单封装。

 

三、Future增强

提供了一个ListenableFuture,其是jdk的Future的封装,用来支持回调(成功/失败),其借鉴了com.google.common.util.concurrent.ListenableFuture。

Java代码  收藏代码
  1. @Test  
  2. public void test() throws Exception {  
  3.     ListenableFutureTask<String> task = new ListenableFutureTask<String>(new Callable() {  
  4.         @Override  
  5.         public Object call() throws Exception {  
  6.             Thread.sleep(10 * 1000L);  
  7.             System.out.println("=======task execute");  
  8.             return "hello";  
  9.         }  
  10.     });  
  11.   
  12.     task.addCallback(new ListenableFutureCallback<String>() {  
  13.         @Override  
  14.         public void onSuccess(String result) {  
  15.             System.out.println("===success callback 1");  
  16.         }  
  17.   
  18.         @Override  
  19.         public void onFailure(Throwable t) {  
  20.         }  
  21.     });  
  22.   
  23.     task.addCallback(new ListenableFutureCallback<String>() {  
  24.         @Override  
  25.         public void onSuccess(String result) {  
  26.             System.out.println("===success callback 2");  
  27.         }  
  28.   
  29.         @Override  
  30.         public void onFailure(Throwable t) {  
  31.         }  
  32.     });  
  33.   
  34.     ExecutorService executorService = Executors.newSingleThreadExecutor();  
  35.     executorService.submit(task);  
  36.     String result = task.get();  
  37.     System.out.println(result);  
  38.   
  39. }  

可以通过addCallback添加一些回调,当执行成功/失败时会自动调用。

 

四、MvcUriComponentsBuilder

MvcUriComponentsBuilder类似于ServletUriComponentsBuilder,但是可以直接从控制器获取URI信息,如下所示:

假设我们的控制器是:

Java代码  收藏代码
  1. @Controller  
  2. @RequestMapping("/user")  
  3. public class UserController {  
  4.   
  5.     @RequestMapping("/{id}")  
  6.     public String view(@PathVariable("id") Long id) {  
  7.         return "view";  
  8.     }  
  9.   
  10.     @RequestMapping("/{id}")  
  11.     public A getUser(@PathVariable("id") Long id) {  
  12.         return new A();  
  13.     }  
  14.   
  15. }  

注:如果在真实mvc环境,存在两个@RequestMapping("/{id}")是错误的。当前只是为了测试。

 

我们可以通过如下方式得到

Java代码  收藏代码
  1. //需要静态导入 import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.*;  
  2. @Test  
  3. public void test() {  
  4.     MockHttpServletRequest req = new MockHttpServletRequest();  
  5.     RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(req));  
  6.   
  7.     //MvcUriComponentsBuilder类似于ServletUriComponentsBuilder,但是直接从控制器获取  
  8.     //类级别的  
  9.     System.out.println(  
  10.             fromController(UserController.class).build().toString()  
  11.     );  
  12.   
  13.     //方法级别的  
  14.     System.out.println(  
  15.             fromMethodName(UserController.class"view", 1L).build().toString()  
  16.     );  
  17.   
  18.     //通过Mock方法调用得到  
  19.     System.out.println(  
  20.             fromMethodCall(on(UserController.class).getUser(2L)).build()  
  21.     );  
  22. }  

注意:当前MvcUriComponentsBuilder实现有问题,只有JDK环境支持,大家可以复制一份,然后修改:

method.getParameterCount() (Java 8才支持)

method.getParameterTypes().length

 

五、Socket支持

提供了获取Socket TCP/UDP可用端口的工具,如

SocketUtils.findAvailableTcpPort()

SocketUtils.findAvailableTcpPort(min, max) 

SocketUtils.findAvailableUdpPort()

非常简单,就不用特别说明了。

 

示例代码请参考:spring4-others

 

到此,spring4新特性就介绍完了,此处没有介绍websocket,后续有时间考虑写一个websocket完整系列,对于spring4除了websocket,其他方面并没有特别吸引人的功能。

 

 

原文链接:https://jinnianshilongnian.iteye.com/blog/1995111

 

 

 

Spring 4.0已经发布RELEASE版本,不仅支持Java8,而且向下兼容到JavaSE6/JavaEE6,并移出了相关废弃类,新添加如Java8的支持、Groovy式Bean定义DSL、对核心容器进行增强、对Web框架的增强、Websocket模块的实现、测试的增强等。其中两个我一直想要的增强就是:支持泛型依赖注入、对cglib类代理不再要求必须有空参构造器了。具体更新请参考:

http://docs.spring.io/spring/docs/4.0.0.RELEASE/spring-framework-reference/htmlsingle/#new-in-4.0

 

1、相关代码:

1.1、实体

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.     private Long id;  
  3.     private String name;  
  4. }  
  5.   
  6. public class Organization implements Serializable {  
  7.     private Long id;  
  8.     private String name;  
  9. }  

 1.2、Repository

Java代码  收藏代码
  1. public abstract class BaseRepository<M extends Serializable> {  
  2.     public void save(M m) {  
  3.         System.out.println("=====repository save:" + m);  
  4.     }  
  5. }  
  6.   
  7. @Repository  
  8. public class UserRepository extends BaseRepository<User> {  
  9. }  
  10.   
  11. @Repository  
  12. public class OrganizationRepository extends BaseRepository<Organization> {  
  13. }  

 对于Repository,我们一般是这样实现的:首先写一个模板父类,把通用的crud等代码放在BaseRepository;然后子类继承后,只需要添加额外的实现。

 

1.3、Service

1.3.1、以前Service写法

Java代码  收藏代码
  1. public abstract class BaseService<M extends Serializable> {  
  2.     private BaseRepository<M> repository;  
  3.     public void setRepository(BaseRepository<M> repository) {  
  4.         this.repository = repository;  
  5.     }  
  6.     public void save(M m) {  
  7.         repository.save(m);  
  8.     }  
  9. }  
  10. @Service  
  11. public class UserService extends BaseService<User> {  
  12.     @Autowired  
  13.     public void setUserRepository(UserRepository userRepository) {  
  14.         setRepository(userRepository);  
  15.     }  
  16. }  
  17.   
  18. @Service  
  19. public class OrganizationService extends BaseService<Organization> {  
  20.     @Autowired  
  21.     public void setOrganizationRepository(OrganizationRepository organizationRepository) {  
  22.         setRepository(organizationRepository);  
  23.     }  
  24. }  

 

可以看到,以前必须再写一个setter方法,然后指定注入的具体类型,然后进行注入;

 

1.3.2、泛型Service的写法

Java代码  收藏代码
  1. public abstract class BaseService<M extends Serializable> {  
  2.     @Autowired  
  3.     protected BaseRepository<M> repository;  
  4.   
  5.     public void save(M m) {  
  6.         repository.save(m);  
  7.     }  
  8. }  
  9.   
  10. @Service  
  11. public class UserService extends BaseService<User> {  
  12. }  
  13.   
  14. @Service  
  15. public class OrganizationService extends BaseService<Organization> {  
  16. }  

 

 大家可以看到,现在的写法非常简洁。支持泛型式依赖注入。

 

这个也是我之前非常想要的一个功能,这样对于那些基本的CRUD式代码,可以简化更多的代码。

 

 

如果大家用过Spring data jpa的话,以后注入的话也可以使用泛型限定式依赖注入 :

Java代码  收藏代码
  1. @Autowired  
  2. private Repository<User> userRepository;  

 

 对于泛型依赖注入,最好使用setter注入,这样万一子类想变,比较容易切换。比如https://github.com/zhangkaitao/es,如果有多个实现时,子类可以使用@Qualifier指定使用哪一个。

posted @ 2019-02-23 12:34  洪墨水  阅读(690)  评论(0编辑  收藏  举报