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代码- public class User implements Serializable {
- private Long id;
- private String name;
- }
- public class Organization implements Serializable {
- private Long id;
- private String name;
- }
1.2、Repository
Java代码- public abstract class BaseRepository<M extends Serializable> {
- public void save(M m) {
- System.out.println("=====repository save:" + m);
- }
- }
- @Repository
- public class UserRepository extends BaseRepository<User> {
- }
- @Repository
- public class OrganizationRepository extends BaseRepository<Organization> {
- }
对于Repository,我们一般是这样实现的:首先写一个模板父类,把通用的crud等代码放在BaseRepository;然后子类继承后,只需要添加额外的实现。
1.3、Service
1.3.1、以前Service写法
Java代码- public abstract class BaseService<M extends Serializable> {
- private BaseRepository<M> repository;
- public void setRepository(BaseRepository<M> repository) {
- this.repository = repository;
- }
- public void save(M m) {
- repository.save(m);
- }
- }
- @Service
- public class UserService extends BaseService<User> {
- @Autowired
- public void setUserRepository(UserRepository userRepository) {
- setRepository(userRepository);
- }
- }
- @Service
- public class OrganizationService extends BaseService<Organization> {
- @Autowired
- public void setOrganizationRepository(OrganizationRepository organizationRepository) {
- setRepository(organizationRepository);
- }
- }
可以看到,以前必须再写一个setter方法,然后指定注入的具体类型,然后进行注入;
1.3.2、泛型Service的写法
Java代码- public abstract class BaseService<M extends Serializable> {
- @Autowired
- protected BaseRepository<M> repository;
- public void save(M m) {
- repository.save(m);
- }
- }
- @Service
- public class UserService extends BaseService<User> {
- }
- @Service
- public class OrganizationService extends BaseService<Organization> {
- }
大家可以看到,现在的写法非常简洁。支持泛型式依赖注入。
这个也是我之前非常想要的一个功能,这样对于那些基本的CRUD式代码,可以简化更多的代码。
如果大家用过Spring data jpa的话,以后注入的话也可以使用泛型限定式依赖注入 :
Java代码- @Autowired
- private Repository<User> userRepository;
对于泛型依赖注入,最好使用setter注入,这样万一子类想变,比较容易切换。比如https://github.com/zhangkaitao/es,如果有多个实现时,子类可以使用@Qualifier指定使用哪一个。
1、Map依赖注入:
Java代码- @Autowired
- 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代码- @Autowired
- private List<BaseService> list;
这样会注入所有实现了BaseService的Bean;但是顺序是不确定的,如果我们想要按照某个顺序获取;在Spring4中可以使用@Order或实现Ordered接口来实现,如:
Java代码- @Order(value = 1)
- @Service
- public class UserService extends BaseService<User> {
- }
这种方式在一些需要多态的场景下是非常有用的。
3、@Lazy可以延迟依赖注入:
Java代码- @Lazy
- @Service
- public class UserService extends BaseService<User> {
- }
- @Lazy
- @Autowired
- private UserService userService;
我们可以把@Lazy放在@Autowired之上,即依赖注入也是延迟的;当我们调用userService时才会注入。即延迟依赖注入到使用时。同样适用于@Bean。
4、@Conditional
@Conditional类似于@Profile(一般用于如我们有开发环境、测试环境、正式机环境,为了方便切换不同的环境可以使用
@Profile指定各个环境的配置,然后通过某个配置来开启某一个环境,方便切换)
,但是@Conditional的优点是允许自己定义规则。可以指定在如@Component、@Bean、@Configuration等注解的类上,以绝对Bean是否创建等。首先来看看使用@Profile的用例,假设我们有个用户模块:
1、在测试/开发期间调用本机的模拟接口方便开发;
2、在部署到正式机时换成调用远程接口;
Java代码- public abstract class UserService extends BaseService<User> {
- }
- @Profile("local")
- @Service
- public class LocalUserService extends UserService {
- }
- @Profile("remote")
- @Service
- public class RemoteUserService extends UserService {
- }
我们在写测试用例时,可以指定我们使用哪个Profile:
Java代码- @ActiveProfiles("remote")
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration(locations = "classpath:spring-config.xml")
- public class ServiceTest {
- @Autowired
- private UserService userService;
- }
这种方式非常简单。如果想自定义如@Profile之类的注解等,那么@Conditional就派上用场了;假设我们系统中有好多本地/远程接口,那么我们定义两个注解@Local和@Remote注解要比使用@Profile方便的多;如:
Java代码
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.TYPE, ElementType.METHOD})
- @Conditional(CustomCondition.class)
- public @interface Local {
- }
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.TYPE, ElementType.METHOD})
- @Conditional(CustomCondition.class)
- public @interface Remote {
- }
Java代码
- public class CustomCondition implements Condition {
- @Override
- public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
- boolean isLocalBean = metadata.isAnnotated("com.sishuok.spring4.annotation.Local");
- boolean isRemoteBean = metadata.isAnnotated("com.sishuok.spring4.annotation.Remote");
- //如果bean没有注解@Local或@Remote,返回true,表示创建Bean
- if(!isLocalBean && !isRemoteBean) {
- return true;
- }
- boolean isLocalProfile = context.getEnvironment().acceptsProfiles("local");
- //如果profile=local 且 bean注解了@Local,则返回true 表示创建bean;
- if(isLocalProfile) {
- return isLocalBean;
- }
- //否则默认返回注解了@Remote或没有注解@Remote的Bean
- return isRemoteBean;
- }
- }
然后我们使用这两个注解分别注解我们的Service:
Java代码- @Local
- @Service
- public class LocalUserService extends UserService {
- }
Java代码
- @Remote
- @Service
- public class RemoteUserService extends UserService {
- }
首先在@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代码
- @Controller
- public class UserController {
- private UserService userService;
- @Autowired
- public UserController(UserService userService) {
- this.userService = userService;
- }
- }
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开始,Spring以Servlet3为进行开发,如果用Spring MVC 测试框架的话需要指定Servlet3兼容的jar包(因为其Mock的对象都是基于Servlet3的)。另外为了方便Rest开发,通过新的@RestController指定在控制器上,这样就不需要在每个@RequestMapping方法上加 @ResponseBody了。而且添加了一个
AsyncRestTemplate
,支持REST客户端的异步无阻塞支持。
1、@RestController
Java代码- @RestController
- public class UserController {
- private UserService userService;
- @Autowired
- public UserController(UserService userService) {
- this.userService = userService;
- }
- @RequestMapping("/test")
- public User view() {
- User user = new User();
- user.setId(1L);
- user.setName("haha");
- return user;
- }
- @RequestMapping("/test2")
- public String view2() {
- return "{\"id\" : 1}";
- }
- }
其实现就是在@@RestController中加入@ResponseBody:
Java代码- @org.springframework.stereotype.Controller
- @org.springframework.web.bind.annotation.ResponseBody
- public @interface RestController {
- }
这样当你开发Rest服务器端的时候,spring-mvc配置文件需要的代码极少,可能就仅需如下一行:
Java代码- <context:component-scan base-package="com.sishuok.spring4"/>
- <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代码- @RestController
- public class UserController {
- private UserService userService;
- @Autowired
- public UserController(UserService userService) {
- this.userService = userService;
- }
- @RequestMapping("/api")
- public Callable<User> api() {
- System.out.println("=====hello");
- return new Callable<User>() {
- @Override
- public User call() throws Exception {
- Thread.sleep(10L * 1000); //暂停两秒
- User user = new User();
- user.setId(1L);
- user.setName("haha");
- return user;
- }
- };
- }
- }
非常简单,服务器端暂停10秒再返回结果(但是服务器也是非阻塞的)。具体参考我github上的代码。
3.2、客户端
Java代码- public static void main(String[] args) {
- AsyncRestTemplate template = new AsyncRestTemplate();
- //调用完后立即返回(没有阻塞)
- ListenableFuture<ResponseEntity<User>> future = template.getForEntity("http://localhost:9080/spring4/api", User.class);
- //设置异步回调
- future.addCallback(new ListenableFutureCallback<ResponseEntity<User>>() {
- @Override
- public void onSuccess(ResponseEntity<User> result) {
- System.out.println("======client get result : " + result.getBody());
- }
- @Override
- public void onFailure(Throwable t) {
- System.out.println("======client failure : " + t);
- }
- });
- System.out.println("==no wait");
- }
此处使用Future来完成非阻塞,这样的话我们也需要给它一个回调接口来拿结果; Future和Callable是一对,一个消费结果,一个产生结果。调用完模板后会立即返回,不会阻塞;有结果时会调用其回调。
AsyncRestTemplate默认使用SimpleClientHttpRequestFactory,即通过java.net.HttpURLConnection实现;另外我们也可以使用apache的http components;使用template.setAsyncRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());设置即可。
另外在开发时尽量不要自己注册如:
Java代码- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
尽量使用
Java代码- <mvc:annotation-driven/>
它设计的已经足够好,使用子元素可以配置我们需要的配置。
且不要使用老版本的:
Java代码- <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
- <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,当然不一定是新特性:
- 集成Bean Validation 1.1到SpringMVC
- 分组验证、分组顺序及级联验证
- 消息中使用EL表达式
- 方法参数/返回值验证
- 自定义验证规则
- 类级别验证器
- 脚本验证器
- cross-parameter,跨参数验证
- 混合类级别验证器和跨参数验证器
- 组合多个验证注解
- 本地化
因为大多数时候验证都配合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代码- <dependency>
- <groupId>org.hibernate</groupId>
- <artifactId>hibernate-validator</artifactId>
- <version>5.0.2.Final</version>
- </dependency>
如果想在消息中使用EL表达式,请确保EL表达式版本是 2.2或以上,如使用Tomcat6,请到Tomcat7中拷贝相应的EL jar包到Tomcat6中。
Java代码- <dependency>
- <groupId>javax.el</groupId>
- <artifactId>javax.el-api</artifactId>
- <version>2.2.4</version>
- <scope>provided</scope>
- </dependency>
请确保您使用的Web容器有相应版本的el jar包。
对于其他POM依赖请下载附件中的项目参考。
1.2、Spring MVC配置文件(spring-mvc.xml):
Java代码- <!-- 指定自己定义的validator -->
- <mvc:annotation-driven validator="validator"/>
- <!-- 以下 validator ConversionService 在使用 mvc:annotation-driven 会 自动注册-->
- <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
- <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
- <!-- 如果不加默认到 使用classpath下的 ValidationMessages.properties -->
- <property name="validationMessageSource" ref="messageSource"/>
- </bean>
- <!-- 国际化的消息资源文件(本系统中主要用于显示/错误消息定制) -->
- <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
- <property name="basenames">
- <list>
- <!-- 在web环境中一定要定位到classpath 否则默认到当前web应用下找 -->
- <value>classpath:messages</value>
- <value>classpath:org/hibernate/validator/ValidationMessages</value>
- </list>
- </property>
- <property name="useCodeAsDefaultMessage" value="false"/>
- <property name="defaultEncoding" value="UTF-8"/>
- <property name="cacheSeconds" value="60"/>
- </bean>
此处主要把bean validation的消息查找委托给spring的messageSource。
1.3、实体验证注解:
Java代码- public class User implements Serializable {
- @NotNull(message = "{user.id.null}")
- private Long id;
- @NotEmpty(message = "{user.name.null}")
- @Length(min = 5, max = 20, message = "{user.name.length.illegal}")
- @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}")
- private String name;
- @NotNull(message = "{user.password.null}")
- private String password;
- }
对于验证规则可以参考官方文档,或者《第七章 注解式控制器的数据验证、类型转换及格式化》。
1.4、错误消息文件messages.properties:
Java代码- user.id.null=用户编号不能为空
- user.name.null=用户名不能为空
- user.name.length.illegal=用户名长度必须在5到20之间
- user.name.illegal=用户名必须是字母
- user.password.null=密码不能为空
1.5、控制器
Java代码- @Controller
- public class UserController {
- @RequestMapping("/save")
- public String save(@Valid User user, BindingResult result) {
- if(result.hasErrors()) {
- return "error";
- }
- return "success";
- }
- }
1.6、错误页面:
Java代码- <spring:hasBindErrors name="user">
- <c:if test="${errors.fieldErrorCount > 0}">
- 字段错误:<br/>
- <c:forEach items="${errors.fieldErrors}" var="error">
- <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/>
- ${error.field}------${message}<br/>
- </c:forEach>
- </c:if>
- <c:if test="${errors.globalErrorCount > 0}">
- 全局错误:<br/>
- <c:forEach items="${errors.globalErrors}" var="error">
- <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/>
- <c:if test="${not empty message}">
- ${message}<br/>
- </c:if>
- </c:forEach>
- </c:if>
- </spring:hasBindErrors>
大家以后可以根据这个做通用的错误消息显示规则。比如我前端页面使用validationEngine显示错误消息,那么我可以定义一个tag来通用化错误消息的显示:showFieldError.tag。
1.7、测试
输入如:http://localhost:9080/spring4/save?name=123 , 我们得到如下错误:
Java代码- name------用户名必须是字母
- name------用户名长度必须在5到20之间
- password------密码不能为空
- id------用户编号不能为空
基本的集成就完成了。
如上测试有几个小问题:
1、错误消息顺序,大家可以看到name的错误消息顺序不是按照书写顺序的,即不确定;
2、我想显示如:用户名【zhangsan】必须在5到20之间;其中我们想动态显示:用户名、min,max;而不是写死了;
3、我想在修改的时候只验证用户名,其他的不验证怎么办。
接下来我们挨着试试吧。
2、分组验证及分组顺序
如果我们想在新增的情况验证id和name,而修改的情况验证name和password,怎么办? 那么就需要分组了。
首先定义分组接口:
Java代码- public interface First {
- }
- public interface Second {
- }
分组接口就是两个普通的接口,用于标识,类似于java.io.Serializable。
接着我们使用分组接口标识实体:
Java代码- public class User implements Serializable {
- @NotNull(message = "{user.id.null}", groups = {First.class})
- private Long id;
- @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {Second.class})
- @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})
- private String name;
- @NotNull(message = "{user.password.null}", groups = {First.class, Second.class})
- private String password;
- }
验证时使用如:
Java代码- @RequestMapping("/save")
- public String save(@Validated({Second.class}) User user, BindingResult result) {
- if(result.hasErrors()) {
- return "error";
- }
- return "success";
- }
即通过@Validate注解标识要验证的分组;如果要验证两个的话,可以这样@Validated({First.class, Second.class})。
接下来我们来看看通过分组来指定顺序;还记得之前的错误消息吗? user.name会显示两个错误消息,而且顺序不确定;如果我们先验证一个消息;如果不通过再验证另一个怎么办?可以通过@GroupSequence指定分组验证顺序:
Java代码
- @GroupSequence({First.class, Second.class, User.class})
- public class User implements Serializable {
- private Long id;
- @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})
- @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})
- private String name;
- private String password;
- }
通过@GroupSequence指定验证顺序:先验证First分组,如果有错误立即返回而不会验证Second分组,接着如果First分组验证通过了,那么才去验证Second分组,最后指定User.class表示那些没有分组的在最后。这样我们就可以实现按顺序验证分组了。
另一个比较常见的就是级联验证:
如:
Java代码- public class User {
- @Valid
- @ConvertGroup(from=First.class, to=Second.class)
- private Organization o;
- }
1、级联验证只要在相应的字段上加@Valid即可,会进行级联验证;@ConvertGroup的作用是当验证o的分组是First时,那么验证o的分组是Second,即分组验证的转换。
3、消息中使用EL表达式
假设我们需要显示如:用户名[NAME]长度必须在[MIN]到[MAX]之间,此处大家可以看到,我们不想把一些数据写死,如NAME、MIN、MAX;此时我们可以使用EL表达式。
如:
Java代码- @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})
错误消息:
Java代码- user.name.length.illegal=用户名长度必须在{min}到{max}之间
其中我们可以使用{验证注解的属性}得到这些值;如{min}得到@Length中的min值;其他的也是类似的。
到此,我们还是无法得到出错的那个输入值,如name=zhangsan。此时就需要EL表达式的支持,首先确定引入EL jar包且版本正确。然后使用如:
Java代码- user.name.length.illegal=用户名[${validatedValue}]长度必须在5到20之间
使用如EL表达式:${validatedValue}得到输入的值,如zhangsan。当然我们还可以使用如${min > 1 ? '大于1' : '小于等于1'},及在EL表达式中也能拿到如@Length的min等数据。
另外我们还可以拿到一个java.util.Formatter类型的formatter变量进行格式化:
Java代码- ${formatter.format("%04d", min)}
4、方法参数/返回值验证
这个可以参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》,概念是类似的,具体可以参考Bean Validation 文档。
5、自定义验证规则
有时候默认的规则可能还不够,有时候还需要自定义规则,比如屏蔽关键词验证是非常常见的一个功能,比如在发帖时帖子中不允许出现admin等关键词。
1、定义验证注解
Java代码- package com.sishuok.spring4.validator;
- import javax.validation.Constraint;
- import javax.validation.Payload;
- import java.lang.annotation.Documented;
- import java.lang.annotation.Retention;
- import java.lang.annotation.Target;
- import static java.lang.annotation.ElementType.*;
- import static java.lang.annotation.RetentionPolicy.*;
- /**
- * <p>User: Zhang Kaitao
- * <p>Date: 13-12-15
- * <p>Version: 1.0
- */
- @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
- @Retention(RUNTIME)
- //指定验证器
- @Constraint(validatedBy = ForbiddenValidator.class)
- @Documented
- public @interface Forbidden {
- //默认错误消息
- String message() default "{forbidden.word}";
- //分组
- Class<?>[] groups() default { };
- //负载
- Class<? extends Payload>[] payload() default { };
- //指定多个时使用
- @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
- @Retention(RUNTIME)
- @Documented
- @interface List {
- Forbidden[] value();
- }
- }
2、 定义验证器
Java代码- package com.sishuok.spring4.validator;
- import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.ApplicationContext;
- import org.springframework.util.StringUtils;
- import javax.validation.ConstraintValidator;
- import javax.validation.ConstraintValidatorContext;
- import java.io.Serializable;
- /**
- * <p>User: Zhang Kaitao
- * <p>Date: 13-12-15
- * <p>Version: 1.0
- */
- public class ForbiddenValidator implements ConstraintValidator<Forbidden, String> {
- private String[] forbiddenWords = {"admin"};
- @Override
- public void initialize(Forbidden constraintAnnotation) {
- //初始化,得到注解数据
- }
- @Override
- public boolean isValid(String value, ConstraintValidatorContext context) {
- if(StringUtils.isEmpty(value)) {
- return true;
- }
- for(String word : forbiddenWords) {
- if(value.contains(word)) {
- return false;//验证失败
- }
- }
- return true;
- }
- }
验证器中可以使用spring的依赖注入,如注入:@Autowired private ApplicationContext ctx;
3、使用
Java代码- public class User implements Serializable {
- @Forbidden()
- private String name;
- }
4、当我们在提交name中含有admin的时候会输出错误消息:
Java代码- forbidden.word=您输入的数据中有非法关键词
问题来了,哪个词是非法的呢?bean validation 和 hibernate validator都没有提供相应的api提供这个数据,怎么办呢?通过跟踪代码,发现一种不是特别好的方法:我们可以覆盖org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl实现(即复制一份代码放到我们的src中),然后覆盖buildAnnotationParameterMap方法;
Java代码- private Map<String, Object> buildAnnotationParameterMap(Annotation annotation) {
- ……
- //将Collections.unmodifiableMap( parameters );替换为如下语句
- return parameters;
- }
即允许这个数据可以修改;然后在ForbiddenValidator中:
Java代码- for(String word : forbiddenWords) {
- if(value.contains(word)) {
- ((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);
- return false;//验证失败
- }
- }
通过((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);添加自己的属性;放到attributes中的数据可以通过${} 获取。然后消息就可以变成:
Java代码- forbidden.word=您输入的数据中有非法关键词【{word}】
这种方式不是很友好,但是可以解决我们的问题。
典型的如密码、确认密码的场景,非常常用;如果没有这个功能我们需要自己写代码来完成;而且经常重复自己。接下来看看bean validation 1.1如何实现的。
6、类级别验证器
6.1、定义验证注解
Java代码- package com.sishuok.spring4.validator;
- import javax.validation.Constraint;
- import javax.validation.Payload;
- import javax.validation.constraints.NotNull;
- import java.lang.annotation.Documented;
- import java.lang.annotation.Retention;
- import java.lang.annotation.Target;
- import static java.lang.annotation.ElementType.*;
- import static java.lang.annotation.RetentionPolicy.*;
- /**
- * <p>User: Zhang Kaitao
- * <p>Date: 13-12-15
- * <p>Version: 1.0
- */
- @Target({ TYPE, ANNOTATION_TYPE})
- @Retention(RUNTIME)
- //指定验证器
- @Constraint(validatedBy = CheckPasswordValidator.class)
- @Documented
- public @interface CheckPassword {
- //默认错误消息
- String message() default "";
- //分组
- Class<?>[] groups() default { };
- //负载
- Class<? extends Payload>[] payload() default { };
- //指定多个时使用
- @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
- @Retention(RUNTIME)
- @Documented
- @interface List {
- CheckPassword[] value();
- }
- }
6.2、 定义验证器
Java代码- package com.sishuok.spring4.validator;
- import com.sishuok.spring4.entity.User;
- import org.springframework.util.StringUtils;
- import javax.validation.ConstraintValidator;
- import javax.validation.ConstraintValidatorContext;
- /**
- * <p>User: Zhang Kaitao
- * <p>Date: 13-12-15
- * <p>Version: 1.0
- */
- public class CheckPasswordValidator implements ConstraintValidator<CheckPassword, User> {
- @Override
- public void initialize(CheckPassword constraintAnnotation) {
- }
- @Override
- public boolean isValid(User user, ConstraintValidatorContext context) {
- if(user == null) {
- return true;
- }
- //没有填密码
- if(!StringUtils.hasText(user.getPassword())) {
- context.disableDefaultConstraintViolation();
- context.buildConstraintViolationWithTemplate("{password.null}")
- .addPropertyNode("password")
- .addConstraintViolation();
- return false;
- }
- if(!StringUtils.hasText(user.getConfirmation())) {
- context.disableDefaultConstraintViolation();
- context.buildConstraintViolationWithTemplate("{password.confirmation.null}")
- .addPropertyNode("confirmation")
- .addConstraintViolation();
- return false;
- }
- //两次密码不一样
- if (!user.getPassword().trim().equals(user.getConfirmation().trim())) {
- context.disableDefaultConstraintViolation();
- context.buildConstraintViolationWithTemplate("{password.confirmation.error}")
- .addPropertyNode("confirmation")
- .addConstraintViolation();
- return false;
- }
- return true;
- }
- }
其中我们通过disableDefaultConstraintViolation禁用默认的约束;然后通过buildConstraintViolationWithTemplate(消息模板)/addPropertyNode(所属属性)/addConstraintViolation定义我们自己的约束。
6.3、使用
Java代码- @CheckPassword()
- public class User implements Serializable {
- }
放到类头上即可。
7、通过脚本验证
Java代码- @ScriptAssert(script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")
- public class User implements Serializable {
- }
通过脚本验证是非常简单而且强大的,lang指定脚本语言(请参考javax.script.ScriptEngineManager JSR-223),alias是在脚本验证中User对象的名字,但是大家会发现一个问题:错误消息怎么显示呢? 在springmvc 中会添加到全局错误消息中,这肯定不是我们想要的,我们改造下吧。
7.1、定义验证注解
Java代码- package com.sishuok.spring4.validator;
- import org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator;
- import java.lang.annotation.Documented;
- import java.lang.annotation.Retention;
- import java.lang.annotation.Target;
- import javax.validation.Constraint;
- import javax.validation.Payload;
- import static java.lang.annotation.ElementType.TYPE;
- import static java.lang.annotation.RetentionPolicy.RUNTIME;
- @Target({ TYPE })
- @Retention(RUNTIME)
- @Constraint(validatedBy = {PropertyScriptAssertValidator.class})
- @Documented
- public @interface PropertyScriptAssert {
- String message() default "{org.hibernate.validator.constraints.ScriptAssert.message}";
- Class<?>[] groups() default { };
- Class<? extends Payload>[] payload() default { };
- String lang();
- String script();
- String alias() default "_this";
- String property();
- @Target({ TYPE })
- @Retention(RUNTIME)
- @Documented
- public @interface List {
- PropertyScriptAssert[] value();
- }
- }
和ScriptAssert没什么区别,只是多了个property用来指定出错后给实体的哪个属性。
7.2、验证器
Java代码- package com.sishuok.spring4.validator;
- import javax.script.ScriptException;
- import javax.validation.ConstraintDeclarationException;
- import javax.validation.ConstraintValidator;
- import javax.validation.ConstraintValidatorContext;
- import com.sishuok.spring4.validator.PropertyScriptAssert;
- import org.hibernate.validator.constraints.ScriptAssert;
- import org.hibernate.validator.internal.util.Contracts;
- import org.hibernate.validator.internal.util.logging.Log;
- import org.hibernate.validator.internal.util.logging.LoggerFactory;
- import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluator;
- import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluatorFactory;
- import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;
- public class PropertyScriptAssertValidator implements ConstraintValidator<PropertyScriptAssert, Object> {
- private static final Log log = LoggerFactory.make();
- private String script;
- private String languageName;
- private String alias;
- private String property;
- private String message;
- public void initialize(PropertyScriptAssert constraintAnnotation) {
- validateParameters( constraintAnnotation );
- this.script = constraintAnnotation.script();
- this.languageName = constraintAnnotation.lang();
- this.alias = constraintAnnotation.alias();
- this.property = constraintAnnotation.property();
- this.message = constraintAnnotation.message();
- }
- public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
- Object evaluationResult;
- ScriptEvaluator scriptEvaluator;
- try {
- ScriptEvaluatorFactory evaluatorFactory = ScriptEvaluatorFactory.getInstance();
- scriptEvaluator = evaluatorFactory.getScriptEvaluatorByLanguageName( languageName );
- }
- catch ( ScriptException e ) {
- throw new ConstraintDeclarationException( e );
- }
- try {
- evaluationResult = scriptEvaluator.evaluate( script, value, alias );
- }
- catch ( ScriptException e ) {
- throw log.getErrorDuringScriptExecutionException( script, e );
- }
- if ( evaluationResult == null ) {
- throw log.getScriptMustReturnTrueOrFalseException( script );
- }
- if ( !( evaluationResult instanceof Boolean ) ) {
- throw log.getScriptMustReturnTrueOrFalseException(
- script,
- evaluationResult,
- evaluationResult.getClass().getCanonicalName()
- );
- }
- if(Boolean.FALSE.equals(evaluationResult)) {
- constraintValidatorContext.disableDefaultConstraintViolation();
- constraintValidatorContext
- .buildConstraintViolationWithTemplate(message)
- .addPropertyNode(property)
- .addConstraintViolation();
- }
- return Boolean.TRUE.equals( evaluationResult );
- }
- private void validateParameters(PropertyScriptAssert constraintAnnotation) {
- Contracts.assertNotEmpty( constraintAnnotation.script(), MESSAGES.parameterMustNotBeEmpty( "script" ) );
- Contracts.assertNotEmpty( constraintAnnotation.lang(), MESSAGES.parameterMustNotBeEmpty( "lang" ) );
- Contracts.assertNotEmpty( constraintAnnotation.alias(), MESSAGES.parameterMustNotBeEmpty( "alias" ) );
- Contracts.assertNotEmpty( constraintAnnotation.property(), MESSAGES.parameterMustNotBeEmpty( "property" ) );
- Contracts.assertNotEmpty( constraintAnnotation.message(), MESSAGES.parameterMustNotBeEmpty( "message" ) );
- }
- }
和之前的类级别验证器类似,就不多解释了,其他代码全部拷贝自org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator。
7.3、使用
Java代码- @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代码- <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
- <property name="validator" ref="validator"/>
- </bean>
8.2、Service
Java代码- @Validated
- @Service
- public class UserService {
- @CrossParameter
- public void changePassword(String password, String confirmation) {
- }
- }
通过@Validated注解UserService表示该类中有需要进行方法参数/返回值验证; @CrossParameter注解方法表示要进行跨参数验证;即验证password和confirmation是否相等。
8.3、验证注解
Java代码- package com.sishuok.spring4.validator;
- //省略import
- @Constraint(validatedBy = CrossParameterValidator.class)
- @Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
- @Retention(RUNTIME)
- @Documented
- public @interface CrossParameter {
- String message() default "{password.confirmation.error}";
- Class<?>[] groups() default { };
- Class<? extends Payload>[] payload() default { };
- }
8.4、验证器
Java代码- package com.sishuok.spring4.validator;
- //省略import
- @SupportedValidationTarget(ValidationTarget.PARAMETERS)
- public class CrossParameterValidator implements ConstraintValidator<CrossParameter, Object[]> {
- @Override
- public void initialize(CrossParameter constraintAnnotation) {
- }
- @Override
- public boolean isValid(Object[] value, ConstraintValidatorContext context) {
- if(value == null || value.length != 2) {
- throw new IllegalArgumentException("must have two args");
- }
- if(value[0] == null || value[1] == null) {
- return true;
- }
- if(value[0].equals(value[1])) {
- return true;
- }
- return false;
- }
- }
其中@SupportedValidationTarget(ValidationTarget.PARAMETERS)表示验证参数; value将是参数列表。
8.5、使用
Java代码- @RequestMapping("/changePassword")
- public String changePassword(
- @RequestParam("password") String password,
- @RequestParam("confirmation") String confirmation, Model model) {
- try {
- userService.changePassword(password, confirmation);
- } catch (ConstraintViolationException e) {
- for(ConstraintViolation violation : e.getConstraintViolations()) {
- System.out.println(violation.getMessage());
- }
- }
- return "success";
- }
调用userService.changePassword方法,如果验证失败将抛出ConstraintViolationException异常,然后得到ConstraintViolation,调用getMessage即可得到错误消息;然后到前台显示即可。
从以上来看,不如之前的使用方便,需要自己对错误消息进行处理。 下一节我们也写个脚本方式的跨参数验证器。
9、混合类级别验证器和跨参数验证器
9.1、验证注解
Java代码- package com.sishuok.spring4.validator;
- //省略import
- @Constraint(validatedBy = {
- CrossParameterScriptAssertClassValidator.class,
- CrossParameterScriptAssertParameterValidator.class
- })
- @Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
- @Retention(RUNTIME)
- @Documented
- public @interface CrossParameterScriptAssert {
- String message() default "error";
- Class<?>[] groups() default { };
- Class<? extends Payload>[] payload() default { };
- String script();
- String lang();
- String alias() default "_this";
- String property() default "";
- ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
- }
此处我们通过@Constraint指定了两个验证器,一个类级别的,一个跨参数的。validationAppliesTo指定为ConstraintTarget.IMPLICIT,表示隐式自动判断。
9.2、验证器
请下载源码查看
9.3、使用
9.3.1、类级别使用
Java代码- @CrossParameterScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")
指定property即可,其他和之前的一样。
9.3.2、跨参数验证
Java代码- @CrossParameterScriptAssert(script = "args[0] == args[1]", lang = "javascript", alias = "args", message = "{password.confirmation.error}")
- public void changePassword(String password, String confirmation) {
- }
通过args[0]==args[1] 来判断是否相等。
这样,我们的验证注解就自动适应两种验证规则了。
10、组合验证注解
有时候,可能有好几个注解需要一起使用,此时就可以使用组合验证注解
Java代码- @Target({ FIELD})
- @Retention(RUNTIME)
- @Documented
- @NotNull(message = "{user.name.null}")
- @Length(min = 5, max = 20, message = "{user.name.length.illegal}")
- @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.length.illegal}")
- @Constraint(validatedBy = { })
- public @interface Composition {
- String message() default "";
- Class<?>[] groups() default { };
- Class<? extends Payload>[] payload() default { };
- }
这样我们验证时只需要:
Java代码- @Composition()
- private String name;
简洁多了。
11、本地化
即根据不同的语言选择不同的错误消息显示。
1、本地化解析器
Java代码- <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
- <property name="cookieName" value="locale"/>
- <property name="cookieMaxAge" value="-1"/>
- <property name="defaultLocale" value="zh_CN"/>
- </bean>
此处使用cookie存储本地化信息,当然也可以选择其他的,如Session存储。
2、设置本地化信息的拦截器
Java代码- <mvc:interceptors>
- <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
- <property name="paramName" value="language"/>
- </bean>
- </mvc:interceptors>
即请求参数中通过language设置语言。
3、消息文件
4、 浏览器输入
http://localhost:9080/spring4/changePassword?password=1&confirmation=2&language=en_US
到此,我们已经完成大部分Bean Validation的功能实验了。对于如XML配置、编程式验证API的使用等对于我们使用SpringMVC这种web环境用处不大,所以就不多介绍了,有兴趣可以自己下载官方文档学习。
- spring4-hibernate_validator5.rar (86.5 KB)
- 下载次数: 1202
五、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代码- <context:component-scan base-package="com.sishuok.spring4"/>
- <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
- <property name="validator" ref="validator"/>
- </bean>
- <mvc:annotation-driven validator="validator"/>
- <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
- <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
- <property name="validationMessageSource" ref="messageSource"/>
- </bean>
注解风格配置
Java代码- @Configuration
- @EnableWebMvc
- @ComponentScan(basePackages = "com.sishuok.spring4")
- public class MvcConfiguration extends WebMvcConfigurationSupport {
- @Override
- protected Validator getValidator() {
- LocalValidatorFactoryBean localValidatorFactoryBean =
- new LocalValidatorFactoryBean();
- localValidatorFactoryBean.setProviderClass(HibernateValidator.class);
- localValidatorFactoryBean.setValidationMessageSource(messageSource());
- return localValidatorFactoryBean;
- }
- }
Groovy DSL风格配置
Java代码- import org.hibernate.validator.HibernateValidator
- import org.springframework.context.support.ReloadableResourceBundleMessageSource
- import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
- beans {
- xmlns context: "http://www.springframework.org/schema/context"
- xmlns mvc: "http://www.springframework.org/schema/mvc"
- context.'component-scan'('base-package': "com,sishuok.spring4")
- mvc.'annotation-driven'('validator': "validator")
- validator(LocalValidatorFactoryBean) {
- providerClass = HibernateValidator.class
- validationMessageSource = ref("messageSource")
- }
- }
因为Spring4 webmvc没有提供用于Web环境的Groovy DSL实现的WebApplicationContext,所以为了在web环境使用,单独写了一个WebGenricGroovyApplicationContext,可以到源码中查找。
可以看到,它们之前差别不是特别大;以上只提取了部分配置,完整的配置可以参考我的github:spring4-showcase
对于注解风格的配置,如果在Servlet3容器中使用的话,可以借助WebApplicationInitializer实现无配置:
Java代码- public class AppInitializer implements WebApplicationInitializer {
- @Override
- public void onStartup(javax.servlet.ServletContext sc) throws ServletException {
- // AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
- // rootContext.register(AppConfig.class);
- // sc.addListener(new ContextLoaderListener(rootContext));
- //2、springmvc上下文
- AnnotationConfigWebApplicationContext springMvcContext = new AnnotationConfigWebApplicationContext();
- springMvcContext.register(MvcConfiguration.class);
- //3、DispatcherServlet
- DispatcherServlet dispatcherServlet = new DispatcherServlet(springMvcContext);
- ServletRegistration.Dynamic dynamic = sc.addServlet("dispatcherServlet", dispatcherServlet);
- dynamic.setLoadOnStartup(1);
- dynamic.addMapping("/");
- //4、CharacterEncodingFilter
- FilterRegistration filterRegistration =
- sc.addFilter("characterEncodingFilter", CharacterEncodingFilter.class);
- filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
- }
- }
到底好还是不好,需要根据自己项目大小等一些因素来衡量。对于Servlet3可以参考我github的示例:servlet3-showcase
对于Groovy风格配置,如果语法足够丰富、Spring内部支持完善,且编辑器支持也非常好的话,也是不错的选择。
二、Groovy Bean定义
接下来我们来看下groovy DSL的具体使用吧:
1、安装环境
Java代码- <dependency>
- <groupId>org.codehaus.groovy</groupId>
- <artifactId>groovy-all</artifactId>
- <version>${groovy.version}</version>
- </dependency>
我使用的groovy版本是2.2.1
2、相关组件类
此处使用Spring Framework官网的hello world,可以前往http://projects.spring.io/spring-framework/ 主页查看
3、Groovy Bean定义配置文件
Java代码- import com.sishuok.spring4.xml.MessageServiceImpl
- import com.sishuok.spring4.xml.MessagePrinter
- beans {
- messageService(MessageServiceImpl) {//名字(类型)
- message = "hello" //注入的属性
- }
- messagePrinter(MessagePrinter, messageService) //名字(类型,构造器参数列表)
- }
从此处可以看到 如果仅仅是简单的Bean定义,确实比XML简洁。
4、测试
如果不测试环境可以这样测试:
Java代码- public class XmlGroovyBeanDefinitionTest1 {
- @Test
- public void test() {
- ApplicationContext ctx = new GenericGroovyApplicationContext("classpath:spring-config-xml.groovy");
- MessagePrinter messagePrinter = (MessagePrinter) ctx.getBean("messagePrinter");
- messagePrinter.printMessage();
- }
- }
使用GenericGroovyApplicationContext加载groovy配置文件。
如果想集成到Spring Test中,可以这样:
Java代码- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration(locations = "classpath:spring-config-xml.groovy", loader = GenericGroovyContextLoader.class)
- public class XmlGroovyBeanDefinitionTest2 {
- @Autowired
- private MessagePrinter messagePrinter;
- @Test
- public void test() {
- messagePrinter.printMessage();
- }
- }
此处需要定义我们自己的bean loader,即从groovy配置文件加载:
Java代码- public class GenericGroovyContextLoader extends AbstractGenericContextLoader {
- @Override
- protected String getResourceSuffix() {
- throw new UnsupportedOperationException(
- "GenericGroovyContextLoader does not support the getResourceSuffix() method");
- }
- @Override
- protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context) {
- return new GroovyBeanDefinitionReader(context);
- }
- }
使用GroovyBeanDefinitionReader来加载groovy配置文件。
到此基本的使用就结束了,还算是比较简洁,但是我们已经注意到了,在纯Java环境做测试还是比较麻烦的。 比如没有给我们写好相关的测试支撑类。另外大家可以前往Spring的github看看在groovy中的单元测试:GroovyBeanDefinitionReaderTests.groovy
再看一下我们使用注解方式呢:
Java代码- @Component
- public class MessageServiceImpl implements MessageService {
- @Autowired
- @Qualifier("message")
- private String message;
- ……
- }
- @Component
- public class MessagePrinter {
- private MessageService messageService;
- @Autowired
- public MessagePrinter(MessageService messageService) {
- this.messageService = messageService;
- }
- ……
- }
此处省略无关代码,需要的话直接去github查看 。点击前往
Groovy配置文件:
Java代码
- beans {
- xmlns context: "http://www.springframework.org/schema/context" //导入命名空间
- context.'component-scan'('base-package': "com.sishuok.spring4") {
- 'exclude-filter'('type': "aspectj", 'expression': "com.sishuok.spring4.xml.*")
- }
- message(String, "hello") {}
- }
- <context:component-scan base-package="com.sishuok.spring4">
- <context:exclude-filter type="aspectj" expression="com.sishuok.spring4.xml.*"/>
- </context:component-scan>
测试方式和之前的一样就不重复了,可以查看XmlGroovyBeanDefinitionTest2.java。
三、Groovy Bean定义 DSL语法
到目前为止,基本的helloworld就搞定了;接下来看看Groovy DSL都支持哪些配置吧:
创建Bean
构造器
Java代码- validator(LocalValidatorFactoryBean) { //名字(类型)
- providerClass = HibernateValidator.class //属性=值
- validationMessageSource = ref("messageSource") //属性 = 引用,当然也支持如 validationMessageSource=messageSource 但是这种方式缺点是messageSource必须在validator之前声明
- }
静态工厂方法
Java代码- def bean = factory(StaticFactory) {
- prop = 1
- }
- bean.factoryMethod = "getInstance"
- bean(StaticFactory) { bean ->
- bean.factoryMethod = "getInstance"
- prop = 1
- }
- beanFactory(Factory)
- bean(beanFactory : "newInstance", "args") {
- prop = 1
- }
- beanFactory(Factory)
- bean("bean"){bean ->
- bean.factoryBean="beanFactory"
- bean.factoryMethod="newInstance"
- prop = 1
- }
依赖注入
属性注入
Java代码- beanName(BeanClass) { //名字(类型)
- str = "123" // 常量直接注入
- bean = ref("bean") //属性 = 引用 ref("bean", true) 这样的话是引用父容器的
- beans = [bean1, bean2] //数组/集合
- props = [key1:"value1", key2:"value2"] // Properties / Map
- }
构造器注入
Java代码- bean(Bean, "args1", "args2")
静态工厂注入/实例工厂注入,请参考创建bean部分
匿名内部Bean
Java代码- outer(OuterBean) {
- prop = 1
- inner = { InnerBean bean -> //匿名内部Bean
- prop =2
- }
- }
- outer(OuterBean) {
- prop = 1
- inner = { bean -> //匿名内部Bean 通过实例工厂方法创建
- bean.factoryBean = "innerBean"
- bean.factoryMethod = "create"
- prop = 2
- }
- }
单例/非单例/作用域
Java代码- singletonBean(Bean1) { bean ->
- bean.singleton = true
- }
- nonSingletonBean(Bean1) { bean ->
- bean.singleton = false
- }
- prototypeBean(Bean1) { bean ->
- bean.scope = "prototype"
- }
其中bean可以理解为xml中的<bean> 标签,即bean定义。
父子Bean
Java代码- parent(Bean1){ bean ->
- bean.'abstract' = true //抽象的
- prop = 123
- }
- child { bean ->
- bean.parent = parent //指定父bean
- }
命名空间
Java代码- xmlns aop:"http://www.springframework.org/schema/aop"
- myAspect(MyAspect)
- aop {
- config("proxy-target-class":true) {
- aspect( id:"test",ref:"myAspect" ) {
- before method:"before", pointcut: "execution(void com.sishuok.spring4..*.*(..))"
- }
- }
- }
Java代码
- xmlns context: "http://www.springframework.org/schema/context"
- context.'component-scan'('base-package': "com.sishuok.spring4") {
- 'exclude-filter'('type': "aspectj", 'expression': "com.sishuok.spring4.xml.*")
- }
Java代码
- xmlns aop:"http://www.springframework.org/schema/aop"
- scopedList(ArrayList) { bean ->
- bean.scope = "haha"
- aop.'scoped-proxy'()
- }
- <bean id="scopedList" class="java.util.ArrayList" scope="haha">
- <aop:scoped-proxy/>
- </bean>
Java代码
- xmlns util:"http://www.springframework.org/schema/util"
- util.list(id : 'list') {
- value 1
- value 2
- }
- <util:list id="list">
- <value>1</value>
- <value>2</value>
- </util:list>
Java代码
- xmlns util:"http://www.springframework.org/schema/util"
- util.map(id : 'map') {
- entry(key : 1, value :1)
- entry('key-ref' : "messageService", 'value-ref' : "messageService")
- }
- <util:map id="map">
- <entry key="1" value="1"/>
- <entry key-ref="messageService" value-ref="messageService"/>
- </util:map>
引入其他配置文件
Java代码- importBeans "classpath:org/springframework/context/groovy/test.xml"
对于DSL新的更新大家可以关注:GroovyBeanDefinitionReaderTests.groovy,本文也是根据其编写的。
再来看看groovy bean定义的另一个好处:
我们可以直接在groovy bean定义文件中声明类,然后使用
Java代码- @Controller
- def class GroovyController {
- @RequestMapping("/groovy")
- @ResponseBody
- public String hello() {
- return "hello";
- }
- }
- beans {
- groovyController(GroovyController)
- }
另一种Spring很早就支持的方式是引入外部groovy文件,如:
Java代码- xmlns lang: "http://www.springframework.org/schema/lang"
- 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代码- ParameterizedType parameterizedType =
- (ParameterizedType) ABService.class.getGenericInterfaces()[0];
- Type genericType = parameterizedType.getActualTypeArguments()[1];
Spring提供的ResolvableType API,提供了更加简单易用的泛型操作支持,如:
Java代码- ResolvableType resolvableType1 = ResolvableType.forClass(ABService.class);
- resolvableType1.as(Service.class).getGeneric(1).resolve()
对于获取更复杂的泛型操作ResolvableType更加简单。
假设我们的API是:
Java代码- public interface Service<N, M> {
- }
- @org.springframework.stereotype.Service
- public class ABService implements Service<A, B> {
- }
- @org.springframework.stereotype.Service
- public class CDService implements Service<C, D> {
- }
如上泛型类非常简单。
1、得到类型的泛型信息
Java代码- ResolvableType resolvableType1 = ResolvableType.forClass(ABService.class);
通过如上API,可以得到类型的ResolvableType,如果类型被Spring AOP进行了CGLIB代理,请使用ClassUtils.getUserClass(ABService.class)得到原始类型。
可以通过如下得到泛型参数的第1个位置(从0开始)的类型信息
Java代码- resolvableType1.getInterfaces()[0].getGeneric(1).resolve()
因为我们泛型信息放在 Service<A, B> 上,所以需要resolvableType1.getInterfaces()[0]得到;
通过getGeneric(泛型参数索引)得到某个位置的泛型;
resolve()把实际泛型参数解析出来
2、得到字段级别的泛型信息
假设我们的字段如下:
Java代码- @Autowired
- private Service<A, B> abService;
- @Autowired
- private Service<C, D> cdService;
- private List<List<String>> list;
- private Map<String, Map<String, Integer>> map;
- private List<String>[] array;
通过如下API可以得到字段级别的ResolvableType
Java代码- ResolvableType resolvableType2 =
- ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "cdService"));
然后通过如下API得到Service<C, D>的第0个位置上的泛型实参类型,即C
Java代码- resolvableType2.getGeneric(0).resolve()
比如 List<List<String>> list;是一种嵌套的泛型用例,我们可以通过如下操作获取String类型:
Java代码- ResolvableType resolvableType3 =
- ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "list"));
- resolvableType3.getGeneric(0).getGeneric(0).resolve();
更简单的写法
Java代码- resolvableType3.getGeneric(0, 0).resolve(); //List<List<String>> 即String
比如Map<String, Map<String, Integer>> map;我们想得到Integer,可以使用:
Java代码- ResolvableType resolvableType4 =
- ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "map"));
- resolvableType4.getGeneric(1).getGeneric(1).resolve();
更简单的写法
Java代码- resolvableType4.getGeneric(1, 1).resolve()
3、得到方法返回值的泛型信息
假设我们的方法如下:
Java代码- private HashMap<String, List<String>> method() {
- return null;
- }
得到Map中的List中的String泛型实参:
Java代码- ResolvableType resolvableType5 =
- ResolvableType.forMethodReturnType(ReflectionUtils.findMethod(GenricInjectTest.class, "method"));
- resolvableType5.getGeneric(1, 0).resolve();
4、得到构造器参数的泛型信息
假设我们的构造器如下:
Java代码- public Const(List<List<String>> list, Map<String, Map<String, Integer>> map) {
- }
我们可以通过如下方式得到第1个参数( Map<String, Map<String, Integer>>)中的Integer:
Java代码- ResolvableType resolvableType6 =
- ResolvableType.forConstructorParameter(ClassUtils.getConstructorIfAvailable(Const.class, List.class, Map.class), 1);
- resolvableType6.getGeneric(1, 0).resolve();
5、得到数组组件类型的泛型信息
如对于private List<String>[] array; 可以通过如下方式获取List的泛型实参String:
Java代码- ResolvableType resolvableType7 =
- ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "array"));
- resolvableType7.isArray();//判断是否是数组
- resolvableType7.getComponentType().getGeneric(0).resolve();
6、自定义泛型类型
Java代码- ResolvableType resolvableType8 = ResolvableType.forClassWithGenerics(List.class, String.class);
- ResolvableType resolvableType9 = ResolvableType.forArrayComponent(resolvableType8);
- 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代码- resolvableType7.isAssignableFrom(resolvableType9)
如下创建一个List<Integer>[]数组,与之前的List<String>[]数组比较,将返回false。
Java代码- ResolvableType resolvableType10 = ResolvableType.forClassWithGenerics(List.class, Integer.class);
- ResolvableType resolvableType11= ResolvableType.forArrayComponent(resolvableType10);
- resolvableType11.getComponentType().getGeneric(0).resolve();
- resolvableType7.isAssignableFrom(resolvableType11);
从如上操作可以看出其泛型操作功能十分完善,尤其在嵌套的泛型信息获取上相当简洁。目前整个Spring4环境都使用这个API来操作泛型信息。
如之前说的泛型注入:Spring4新特性——泛型限定式依赖注入,通过在依赖注入时使用如下类实现:
GenericTypeAwareAutowireCandidateResolver
QualifierAnnotationAutowireCandidateResolver
ContextAnnotationAutowireCandidateResolver
还有如Spring的核心BeanWrapperImpl,以及整个Spring/SpringWevMVC的泛型操作都是替换为这个API了:GenericCollectionTypeResolver和GenericTypeResolver都直接委托给ResolvableType这个API。
所以大家以后对泛型操作可以全部使用这个API了,非常好用。测试用例请参考GenricInjectTest.java。
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代码- //时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。
- Clock c1 = Clock.systemUTC(); //系统默认UTC时钟(当前瞬时时间 System.currentTimeMillis())
- System.out.println(c1.millis()); //每次调用将返回当前瞬时时间(UTC)
- Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)
- Clock c31 = Clock.system(ZoneId.of("Europe/Paris")); //巴黎时区
- System.out.println(c31.millis()); //每次调用将返回当前瞬时时间(UTC)
- Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));//上海时区
- System.out.println(c32.millis());//每次调用将返回当前瞬时时间(UTC)
- Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));//固定上海时区时钟
- System.out.println(c4.millis());
- Thread.sleep(1000);
- System.out.println(c4.millis()); //不变 即时钟时钟在那一个点不动
- Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟
- System.out.println(c1.millis());
- System.out.println(c5.millis());
Instant
瞬时时间,等价于以前的System.currentTimeMillis()
Java代码- //瞬时时间 相当于以前的System.currentTimeMillis()
- Instant instant1 = Instant.now();
- System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于1970-01-01 00:00:00 UTC的一个时间
- System.out.println(instant1.toEpochMilli()); //精确到毫秒
- Clock clock1 = Clock.systemUTC(); //获取系统UTC默认时钟
- Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间
- System.out.println(instant2.toEpochMilli());
- Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟
- Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间
- System.out.println(instant3.toEpochMilli());//equals instant1
LocalDateTime、LocalDate、LocalTime
提供了对java.util.Date的替代,另外还提供了新的DateTimeFormatter用于对格式化/解析的支持
Java代码- //使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()默认时区
- LocalDateTime now = LocalDateTime.now();
- System.out.println(now);
- //自定义时区
- LocalDateTime now2= LocalDateTime.now(ZoneId.of("Europe/Paris"));
- System.out.println(now2);//会以相应的时区显示日期
- //自定义时钟
- Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));
- LocalDateTime now3= LocalDateTime.now(clock);
- System.out.println(now3);//会以相应的时区显示日期
- //不需要写什么相对时间 如java.util.Date 年是相对于1900 月是从0开始
- //2013-12-31 23:59
- LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
- //年月日 时分秒 纳秒
- LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59,59, 11);
- //使用瞬时时间 + 时区
- Instant instant = Instant.now();
- LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
- System.out.println(d3);
- //解析String--->LocalDateTime
- LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");
- System.out.println(d4);
- LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");//999毫秒 等价于999000000纳秒
- System.out.println(d5);
- //使用DateTimeFormatter API 解析 和 格式化
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
- LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);
- System.out.println(formatter.format(d6));
- //时间获取
- System.out.println(d6.getYear());
- System.out.println(d6.getMonth());
- System.out.println(d6.getDayOfYear());
- System.out.println(d6.getDayOfMonth());
- System.out.println(d6.getDayOfWeek());
- System.out.println(d6.getHour());
- System.out.println(d6.getMinute());
- System.out.println(d6.getSecond());
- System.out.println(d6.getNano());
- //时间增减
- LocalDateTime d7 = d6.minusDays(1);
- LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
- //LocalDate 即年月日 无时分秒
- //LocalTime即时分秒 无年月日
- //API和LocalDateTime类似就不演示了
ZonedDateTime
带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义);API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])
Java代码- //即带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义)。
- //API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])
- ZonedDateTime now = ZonedDateTime.now();
- System.out.println(now);
- ZonedDateTime now2= ZonedDateTime.now(ZoneId.of("Europe/Paris"));
- System.out.println(now2);
- //其他的用法也是类似的 就不介绍了
- ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");
- System.out.println(z1);
Duration
表示两个瞬时时间的时间段
Java代码
- //表示两个瞬时时间的时间段
- Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());
- //得到相应的时差
- System.out.println(d1.toDays());
- System.out.println(d1.toHours());
- System.out.println(d1.toMinutes());
- System.out.println(d1.toMillis());
- System.out.println(d1.toNanos());
- //1天时差 类似的还有如ofHours()
- Duration d2 = Duration.ofDays(1);
- System.out.println(d2.toDays());
Chronology
用于对年历系统的支持,是java.util.Calendar的替代者
Java代码- //提供对java.util.Calendar的替换,提供对年历系统的支持
- Chronology c = HijrahChronology.INSTANCE;
- ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
- System.out.println(d);
其他
如果提供了年、年月、月日、周期的API支持
Java代码- Year year = Year.now();
- YearMonth yearMonth = YearMonth.now();
- MonthDay monthDay = MonthDay.now();
- System.out.println(year);//年
- System.out.println(yearMonth); //年-月
- System.out.println(monthDay); // 月-日
- //周期,如表示10天前 3年5个月钱
- Period period1 = Period.ofDays(10);
- System.out.println(period1);
- Period period2 = Period.of(3, 5, 0);
- 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代码- @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private LocalDateTime dateTime;
- @DateTimeFormat(pattern = "yyyy-MM-dd")
- private LocalDate date;
- @DateTimeFormat(pattern = "HH:mm:ss")
- private LocalTime time;
比如我们在springmvc中:
Java代码- @RequestMapping("/test")
- public String test(@ModelAttribute("entity") Entity entity) {
- return "test";
- }
当前端页面请求:
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代码- protected TimeZone determineDefaultTimeZone(HttpServletRequest request) {
- return getDefaultTimeZone();
- }
另外还提供了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代码- Annotation service = AnnotationUtils.findAnnotation(ABService.class, org.springframework.stereotype.Service.class);
- Annotation component = AnnotationUtils.getAnnotation(service, org.springframework.stereotype.Component.class);
获取重复注解:
比如在使用hibernate validation时,我们想在一个方法上加相同的注解多个,需要使用如下方式:
Java代码- @Length.List(
- value = {
- @Length(min = 1, max = 2, groups = A.class),
- @Length(min = 3, max = 4, groups = B.class)
- }
- )
- public void test() {
可以通过如下方式获取@Length:
Java代码- Method method = ClassUtils.getMethod(AnnotationUtilsTest.class, "test");
- Set<Length> set = AnnotationUtils.getRepeatableAnnotation(method, Length.List.class, Length.class);
当然,如果你使用Java8,那么本身就支持重复注解,比如spring的任务调度注解,
Java代码- @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Repeatable(Schedules.class)
- public @interface Scheduled {
- @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface Schedules {
- Scheduled[] value();
- }
这样的话,我们可以直接同时注解相同的多个注解:
Java代码- @Scheduled(cron = "123")
- @Scheduled(cron = "234")
- public void test
但是获取的时候还是需要使用如下方式:
Java代码- AnnotationUtils.getRepeatableAnnotation(ClassUtils.getMethod(TimeTest.class, "test"), Schedules.class, Scheduled.class)
ApplicationContext和BeanFactory提供了直接通过注解获取Bean的方法:
Java代码- @Test
- public void test() {
- AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
- ctx.register(GenericConfig.class);
- ctx.refresh();
- Map<String, Object> beans = ctx.getBeansWithAnnotation(org.springframework.stereotype.Service.class);
- System.out.println(beans);
- }
这样可以实现一些特殊功能。
另外和提供了一个AnnotatedElementUtils用于简化java.lang.reflect.AnnotatedElement的操作,具体请参考其javadoc。
二、脚本的支持
spring4也提供了类似于javax.script的简单封装,用于支持一些脚本语言,核心接口是:
Java代码- public interface ScriptEvaluator {
- Object evaluate(ScriptSource script) throws ScriptCompilationException;
- Object evaluate(ScriptSource script, Map<String, Object> arguments) throws ScriptCompilationException;
- }
比如我们使用groovy脚本的话,可以这样:
Java代码- @Test
- public void test() throws ExecutionException, InterruptedException {
- ScriptEvaluator scriptEvaluator = new GroovyScriptEvaluator();
- //ResourceScriptSource 外部的
- ScriptSource source = new StaticScriptSource("i+j");
- Map<String, Object> args = new HashMap<>();
- args.put("i", 1);
- args.put("j", 2);
- System.out.println(scriptEvaluator.evaluate(source, args));
- }
没什么很特别的地方。另外还提供了BeanShell(BshScriptEvaluator)和javax.script(StandardScriptEvaluator)的简单封装。
三、Future增强
提供了一个ListenableFuture,其是jdk的Future的封装,用来支持回调(成功/失败),其借鉴了com.google.common.util.concurrent.ListenableFuture。
Java代码- @Test
- public void test() throws Exception {
- ListenableFutureTask<String> task = new ListenableFutureTask<String>(new Callable() {
- @Override
- public Object call() throws Exception {
- Thread.sleep(10 * 1000L);
- System.out.println("=======task execute");
- return "hello";
- }
- });
- task.addCallback(new ListenableFutureCallback<String>() {
- @Override
- public void onSuccess(String result) {
- System.out.println("===success callback 1");
- }
- @Override
- public void onFailure(Throwable t) {
- }
- });
- task.addCallback(new ListenableFutureCallback<String>() {
- @Override
- public void onSuccess(String result) {
- System.out.println("===success callback 2");
- }
- @Override
- public void onFailure(Throwable t) {
- }
- });
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- executorService.submit(task);
- String result = task.get();
- System.out.println(result);
- }
可以通过addCallback添加一些回调,当执行成功/失败时会自动调用。
四、MvcUriComponentsBuilder
MvcUriComponentsBuilder类似于ServletUriComponentsBuilder,但是可以直接从控制器获取URI信息,如下所示:
假设我们的控制器是:
Java代码- @Controller
- @RequestMapping("/user")
- public class UserController {
- @RequestMapping("/{id}")
- public String view(@PathVariable("id") Long id) {
- return "view";
- }
- @RequestMapping("/{id}")
- public A getUser(@PathVariable("id") Long id) {
- return new A();
- }
- }
注:如果在真实mvc环境,存在两个@RequestMapping("/{id}")是错误的。当前只是为了测试。
我们可以通过如下方式得到
Java代码- //需要静态导入 import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.*;
- @Test
- public void test() {
- MockHttpServletRequest req = new MockHttpServletRequest();
- RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(req));
- //MvcUriComponentsBuilder类似于ServletUriComponentsBuilder,但是直接从控制器获取
- //类级别的
- System.out.println(
- fromController(UserController.class).build().toString()
- );
- //方法级别的
- System.out.println(
- fromMethodName(UserController.class, "view", 1L).build().toString()
- );
- //通过Mock方法调用得到
- System.out.println(
- fromMethodCall(on(UserController.class).getUser(2L)).build()
- );
- }
注意:当前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代码- public class User implements Serializable {
- private Long id;
- private String name;
- }
- public class Organization implements Serializable {
- private Long id;
- private String name;
- }
1.2、Repository
Java代码- public abstract class BaseRepository<M extends Serializable> {
- public void save(M m) {
- System.out.println("=====repository save:" + m);
- }
- }
- @Repository
- public class UserRepository extends BaseRepository<User> {
- }
- @Repository
- public class OrganizationRepository extends BaseRepository<Organization> {
- }
对于Repository,我们一般是这样实现的:首先写一个模板父类,把通用的crud等代码放在BaseRepository;然后子类继承后,只需要添加额外的实现。
1.3、Service
1.3.1、以前Service写法
Java代码- public abstract class BaseService<M extends Serializable> {
- private BaseRepository<M> repository;
- public void setRepository(BaseRepository<M> repository) {
- this.repository = repository;
- }
- public void save(M m) {
- repository.save(m);
- }
- }
- @Service
- public class UserService extends BaseService<User> {
- @Autowired
- public void setUserRepository(UserRepository userRepository) {
- setRepository(userRepository);
- }
- }
- @Service
- public class OrganizationService extends BaseService<Organization> {
- @Autowired
- public void setOrganizationRepository(OrganizationRepository organizationRepository) {
- setRepository(organizationRepository);
- }
- }
可以看到,以前必须再写一个setter方法,然后指定注入的具体类型,然后进行注入;
1.3.2、泛型Service的写法
Java代码- public abstract class BaseService<M extends Serializable> {
- @Autowired
- protected BaseRepository<M> repository;
- public void save(M m) {
- repository.save(m);
- }
- }
- @Service
- public class UserService extends BaseService<User> {
- }
- @Service
- public class OrganizationService extends BaseService<Organization> {
- }
大家可以看到,现在的写法非常简洁。支持泛型式依赖注入。
这个也是我之前非常想要的一个功能,这样对于那些基本的CRUD式代码,可以简化更多的代码。
如果大家用过Spring data jpa的话,以后注入的话也可以使用泛型限定式依赖注入 :
Java代码- @Autowired
- private Repository<User> userRepository;
对于泛型依赖注入,最好使用setter注入,这样万一子类想变,比较容易切换。比如https://github.com/zhangkaitao/es,如果有多个实现时,子类可以使用@Qualifier指定使用哪一个。
标签:Java,String,代码,System,特性,class,public,Spring4 来源: https://www.cnblogs.com/hongmoshui/p/10422270.html