? Spring技術原理系列(7)帶你看看那些可能你還不知道的Spring特性技巧哦!

浩宇天尚發表於2021-10-17

前提介紹

本文主要介紹相關Spring框架的一些新特性問題機制,包含了一些特定註解方面的認識。

@Lazy可以延遲依賴注入

@Lazy註解修飾在類層面!

@Lazy
@Service
public class UserService extends BaseService<User> { }

可以把@Lazy放在@Autowired之上,即依賴注入也是延遲的;當我們呼叫userService時才會注入。即延遲依賴注入到使用時。同樣適用於@Bean。

@Lazy
@Autowired
private UserService userService;

@Conditional

@Conditional類似於@Profile

  • 一般用於如有開發環境、測試環境、正式機環境,為了方便切換不同的環境可以使用@Profile指定各個環境的配置。

  • 通過某個配置來開啟某個環境,方便切換,但是@Conditional的優點是允許自己定義規則,可以指定在如@Component、@Bean、@Configuration等註解的類上,以絕對Bean是否建立等。

首先來看看使用@Profile的用例,假設我們有個使用者模組:

  1. 在測試/開發期間呼叫本機的模擬介面方便開發;
  2. 在部署到正式機時換成呼叫遠端介面;
public abstract class UserService extends BaseService<User> { }

@Profile("local")
@Service
public class LocalUserService extends UserService {}

@Profile("remote")
@Service
public 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
@Service
public class LocalUserService extends UserService { }
@Remote
@Service
public class RemoteUserService extends UserService {}
  • 首先在@Local和@Remote註解上使用@Conditional(CustomCondition.class)指定條件。

  • 然後使用@Local和@Remote註解我們的Service,這樣當載入Service時,會先執行條件然後判斷是否載入為Bean。

@Profile實現的Condition是:org.springframework.context.annotation.ProfileCondition。

AsyncRestTemplate非阻塞非同步(已廢棄WebClient代替之)

提供AsyncRestTemplate用於客戶端非阻塞非同步支援。

伺服器端
@RestController
public 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。

@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新增一些回撥,當執行成功/失敗時會自動呼叫。

  • 此處使用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.Service
public class ABService implements Service<A, B> { }
@org.springframework.stereotype.Service
public 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> 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[] 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[]陣列,與之前的List[]陣列比較,將返回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)  
@Documented
public @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的方法:

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

ScriptEvaluator指令碼的支援

spring也提供了類似於javax.script的簡單封裝,用於支援一些指令碼語言,核心介面是:

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

比如我們使用groovy指令碼的話,可以這樣:

@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)的簡單封裝。

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()

相關文章