写点什么

👊 【Spring 技术特性】带你看看那些可能你还不知道的特性技巧哦!

发布于: 22 小时前
👊 【Spring技术特性】带你看看那些可能你还不知道的特性技巧哦!

前提介绍

本文主要介绍相关 Spring 框架的一些新特性问题机制,包含了一些特定注解方面的认识。

@Lazy 可以延迟依赖注入

@Lazy 注解修饰在类层面!


@Lazy@Servicepublic class UserService extends BaseService<User> { }
复制代码


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


@Lazy@Autowiredprivate UserService userService;
复制代码

@Conditional

@Conditional 类似于 @Profile


  • 一般用于如有开发环境、测试环境、正式机环境,为了方便切换不同的环境可以使用 @Profile 指定各个环境的配置。

  • 通过某个配置来开启某个环境,方便切换,但是 @Conditional 的优点是允许自己定义规则,可以指定在如 @Component、@Bean、@Configuration 等注解的类上,以绝对 Bean 是否创建等。


首先来看看使用 @Profile 的用例,假设我们有个用户模块:


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

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


public abstract class UserService extends BaseService<User> { }
@Profile("local")@Servicepublic class LocalUserService extends UserService {}
@Profile("remote")@Servicepublic class RemoteUserService extends UserService {}
复制代码


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


@ActiveProfiles("remote")@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations =  "classpath:spring-config.xml")public class ServiceTest {    @Autowired    private UserService userService;}
复制代码


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


 @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 {}
复制代码


public class CustomCondition implements Condition {    @Override    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {        boolean isLocalBean = metadata.isAnnotated("com.xxx.Local");        boolean isRemoteBean = metadata.isAnnotated("com.xxx.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:


@Local@Servicepublic class LocalUserService extends UserService { }@Remote@Servicepublic class RemoteUserService extends UserService {}
复制代码


  • 首先在 @Local 和 @Remote 注解上使用 @Conditional(CustomCondition.class)指定条件。

  • 然后使用 @Local 和 @Remote 注解我们的 Service,这样当加载 Service 时,会先执行条件然后判断是否加载为 Bean。


@Profile 实现的 Condition 是:org.springframework.context.annotation.ProfileCondition。

AsyncRestTemplate 非阻塞异步(已废弃 WebClient 代替之)

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

服务器端
@RestControllerpublic class UserController {    private UserService userService;    @Autowired    public UserController(UserService userService) {        this.userService = userService;    }    @RequestMapping("/api")      public Callable<User> api() {        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 秒再返回结果(但是服务器也是非阻塞的)。

客户端

public static void main(String[] args) {    AsyncRestTemplate template = new AsyncRestTemplate();    //调用完后立即返回(没有阻塞)    ListenableFuture<ResponseEntity<User>> future = template.getForEntity("http://localhost:9080/rest/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 增强,提供了一个 ListenableFuture,其是 jdk 的 Future 的封装,用来支持回调(成功/失败),借鉴了 com.google.common.util.concurrent.ListenableFuture。


@Testpublic 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 添加一些回调,当执行成功/失败时会自动调用。

  • 此处使用 Future 来完成非阻塞,这样的话我们也需要给它一个回调接口来拿结果;

  • Future 和 Callable 是一对,一个消费结果,一个产生结果。调用完模板后会立即返回,不会阻塞;有结果时会调用其回调。




  • AsyncRestTemplate 默认使用 SimpleClientHttpRequestFactory,即通过 java.net.HttpURLConnection 实现;

  • 另外可以使用 apache 的 http components,使用 template.setAsyncRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory()),设置即可。

Spring 对 Java8 的时间类型支持

对 jsr310 的支持,只要能发现 java.time.LocalDate,DefaultFormattingConversionService 就会自动注册对 jsr310 的支持,只需要在实体/Bean 上使用 DateTimeFormat 注解:


@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 中:


@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 中的如下方法来得到:


protected TimeZone determineDefaultTimeZone(HttpServletRequest request) {      return getDefaultTimeZone();  } 
复制代码


  • 另外还提供了 DateTimeContextHolder,其用于线程绑定 DateTimeContext;而 DateTimeContext 提供了如:Chronology、ZoneId、DateTimeFormatter 等上下文数据,如果需要这种上下文信息的话,可以使用这个 API 进行绑定。

  • 比如在进行日期格式化时,就会去查找相应的 DateTimeFormatter,因此如果想自定义相应的格式化格式,那么使用 DateTimeContextHolder 绑定即可。

泛型操作控制

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


ParameterizedType parameterizedType =       (ParameterizedType) ABService.class.getGenericInterfaces()[0];  Type genericType = parameterizedType.getActualTypeArguments()[1];  
复制代码


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

接口层的泛型处理
ResolvableType resolvableType1 = ResolvableType.forClass(ABService.class);resolvableType1.as(Service.class).getGeneric(1).resolve();
复制代码


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


假设我们的 API 是:


public interface Service<N, M> { }@org.springframework.stereotype.Servicepublic class ABService implements Service<A, B> { }@org.springframework.stereotype.Servicepublic class CDService implements Service<C, D> {}
复制代码


得到类型的泛型信息


ResolvableType resolvableType1 = ResolvableType.forClass(ABService.class);
复制代码


通过如上 API,可以得到类型的 ResolvableType,如果类型被 Spring AOP 进行了 CGLIB 代理,请使用ClassUtils.getUserClass(ABService.class)得到原始类型,可以通过如下得到泛型参数的第 1 个位置(从 0 开始)的类型信息


resolvableType1.getInterfaces()[0].getGeneric(1).resolve()
复制代码


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

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


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

得到字段级别的泛型信息

假设我们的字段如下:


@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


ResolvableType resolvableType2 =                  ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "cdService"));  
复制代码


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


resolvableType2.getGeneric(0).resolve()
复制代码


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


ResolvableType resolvableType3 =                ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "list"));resolvableType3.getGeneric(0).getGeneric(0).resolve();
复制代码


更简单的写法


resolvableType3.getGeneric(0, 0).resolve(); //List<List<String>> 即String
复制代码


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


ResolvableType resolvableType4 =                ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "map"));resolvableType4.getGeneric(1).getGeneric(1).resolve();
复制代码


更简单的写法


resolvableType4.getGeneric(1, 1).resolve()  
复制代码

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

private HashMap<String, List<String>> method() {      return null;  }  
复制代码


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


ResolvableType resolvableType5 = ResolvableType.forMethodReturnType(ReflectionUtils.findMethod(GenricInjectTest.class, "method"));resolvableType5.getGeneric(1, 0).resolve();
复制代码

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

假设我们的构造器如下:


public Const(List<List<String>> list, Map<String, Map<String, Integer>> map) {  }
复制代码


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


ResolvableType resolvableType6 = ResolvableType.forConstructorParameter(ClassUtils.getConstructorIfAvailable(Const.class, List.class, Map.class), 1);resolvableType6.getGeneric(1, 0).resolve();
复制代码

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

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


ResolvableType resolvableType7 = ResolvableType.forField(ReflectionUtils.findField(GenricInjectTest.class, "array"));resolvableType7.isArray();//判断是否是数组  resolvableType7.getComponentType().getGeneric(0).resolve();  
复制代码

自定义泛型类型

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():得到相应的泛型信息;
复制代码

泛型等价比较:

resolvableType7.isAssignableFrom(resolvableType9)
复制代码


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


ResolvableType resolvableType10 = ResolvableType.forClassWithGenerics(List.class, Integer.class);ResolvableType resolvableType11= ResolvableType.forArrayComponent(resolvableType10);  resolvableType11.getComponentType().getGeneric(0).resolve();  resolvableType7.isAssignableFrom(resolvableType11);  
复制代码


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

注解方面的改进

Spring 对注解 API 和 ApplicationContext 获取注解 Bean 做了一点改进,取注解的注解,如 @Service 是被 @Compent 注解的注解,可以通过如下方式获取 @Componet 注解实例:


Annotation service = AnnotationUtils.findAnnotation(ABService.class, org.springframework.stereotype.Service.class);  Annotation component = AnnotationUtils.getAnnotation(service, org.springframework.stereotype.Component.class);  
复制代码
获取重复注解:

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


@Length.List(          value = {                  @Length(min = 1, max = 2, groups = A.class),                  @Length(min = 3, max = 4, groups = B.class)          }  )  public void test() {}
复制代码


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


Method method = ClassUtils.getMethod(AnnotationUtilsTest.class, "test");  Set<Length> set = AnnotationUtils.getRepeatableAnnotation(method, Length.List.class, Length.class);
复制代码


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


@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) @Documentedpublic @interface Schedules { Scheduled[] value();}
复制代码


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


@Scheduled(cron = "123")@Scheduled(cron = "234")public void test
复制代码


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


AnnotationUtils.getRepeatableAnnotation(ClassUtils.getMethod(TimeTest.class, "test"), Schedules.class, Scheduled.class)
复制代码


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


@Testpublic 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 的操作。

ScriptEvaluator 脚本的支持

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


public interface ScriptEvaluator {    Object evaluate(ScriptSource script) throws ScriptCompilationException;    Object evaluate(ScriptSource script, Map<String, Object> arguments) throws ScriptCompilationException;}
复制代码


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


@Testpublic 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)的简单封装。

MvcUriComponentsBuilder

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


@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}")是错误的。当前只是为了测试。


  1. 需要静态导入 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()
复制代码


发布于: 22 小时前阅读数: 20
用户头像

🏆 2021年InfoQ写作平台-签约作者 🏆 2020.03.25 加入

【个人简介】酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“ 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、APM专题及微服务/分布式体系等

评论

发布
暂无评论
👊 【Spring技术特性】带你看看那些可能你还不知道的特性技巧哦!