Spring中那些讓你愛不釋手的代碼技巧

前言

上一篇文章《spring中這些能昇華代碼的技巧,可能會讓你愛不釋手》發表之後,受到了不少讀者的好評,很多讀者都在期待續集。今天非常高興的通知大家,你們要的續集來了。本文繼續總結我認爲spring中還不錯的知識點,希望對您有所幫助。

一. @Conditional的強大之處

不知道你們有沒有遇到過這些問題:

  1. 某個功能需要根據項目中有沒有某個jar判斷是否開啓該功能。
  2. 某個bean的實例化需要先判斷另一個bean有沒有實例化,再判斷是否實例化自己。
  3. 某個功能是否開啓,在配置文件中有個參數可以對它進行控制。

如果你有遇到過上述這些問題,那麼恭喜你,本節內容非常適合你。

@ConditionalOnClass

問題1可以用@ConditionalOnClass註解解決,代碼如下:

public  class A {
}

public class B {
}

@ConditionalOnClass(B.class)
@Configuration
public class TestConfiguration
{

@Bean
public A a() {
return new A();
}
}

如果項目中存在B類,則會實例化A類。如果不存在B類,則不會實例化A類。

有人可能會問:不是判斷有沒有某個jar嗎?怎麼現在判斷某個類了?

直接判斷有沒有該jar下的某個關鍵類更簡單。

這個註解有個升級版的應用場景:比如common工程中寫了一個發消息的工具類mqTemplate,業務工程引用了common工程,只需再引入消息中間件,比如rocketmq的jar包,就能開啓mqTemplate的功能。而如果有另一個業務工程,通用引用了common工程,如果不需要發消息的功能,不引入rocketmq的jar包即可。

這個註解的功能還是挺實用的吧?

@ConditionalOnBean

問題2可以通過@ConditionalOnBean註解解決,代碼如下:

@Configuration
public class TestConfiguration {

@Bean
public B b() {
return new B();
}

@ConditionalOnBean(name="b")
@Bean
public A a() {
return new A();
}
}

實例A只有在實例B存在時,才能實例化。

@ConditionalOnProperty

問題3可以通過@ConditionalOnProperty註解解決,代碼如下:

@ConditionalOnProperty(prefix = "demo",name="enable", havingValue = "true",matchIfMissing=true )
@Configuration
public class TestConfiguration {

@Bean
public A a() {
return new A();
}
}

在applicationContext.properties文件中配置參數:

demo.enable=false

各參數含義:

  • prefix 表示參數名的前綴,這裏是demo
  • name 表示參數名
  • havingValue 表示指定的值,參數中配置的值需要跟指定的值比較是否相等,相等才滿足條件
  • matchIfMissing 表示是否允許缺省配置。

這個功能可以作爲開關,相比EnableXXX註解的開關更優雅,因爲它可以通過參數配置是否開啓,而EnableXXX註解的開關需要在代碼中硬編碼開啓或關閉。

其他的Conditional註解

當然,spring用得比較多的Conditional註解還有:ConditionalOnMissingClassConditionalOnMissingBeanConditionalOnWebApplication等。

下面用一張圖整體認識一下@Conditional家族。

自定義Conditional

說實話,個人認爲springboot自帶的Conditional系列已經可以滿足我們絕大多數的需求了。但如果你有比較特殊的場景,也可以自定義自定義Conditional。

第一步,自定義註解:

@Conditional(MyCondition.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(
{ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface MyConditionOnProperty {
String name() default "";

String havingValue() default "";
}

第二步,實現Condition接口:

public  class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println("實現自定義邏輯");
return false;
}
}

第三步,使用@MyConditionOnProperty註解。

Conditional的奧祕就藏在ConfigurationClassParser類的processConfigurationClass方法中:


這個方法邏輯不復雜:

  1. 先判斷有沒有使用Conditional註解,如果沒有直接返回false

  2. 收集condition到集合中

  3. order排序該集合

  4. 遍歷該集合,循環調用conditionmatchs方法。


二. 如何妙用@Import?

有時我們需要在某個配置類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import註解完成這個功能。

如果你看過它的源碼會發現,引入的類支持三種不同類型。

但是我認爲最好將普通類和@Configuration註解的配置類分開講解,所以列了四種不同類型:

普通類

這種引入方式是最簡單的,被引入的類會被實例化bean對象。

public  class A {
}

@Import(A.class)
@Configuration
public class TestConfiguration
{
}

通過@Import註解引入A類,spring就能自動實例化A對象,然後在需要使用的地方通過@Autowired註解注入即可:

   @Autowired
private A a;

是不是挺讓人意外的?不用加@Bean註解也能實例化bean。

@Configuration註解的配置類

這種引入方式是最複雜的,因爲@Configuration註解還支持多種組合註解,比如:

  • @Import
  • @ImportResource
  • @PropertySource等。
public  class A {
}

public class B {
}

@Import(B.class)
@Configuration
public class AConfiguration
{

@Bean
public A a() {
return new A();
}
}

@Import(AConfiguration.class)
@Configuration
public class TestConfiguration
{
}

通過@Import註解引入@Configuration註解的配置類,會把該配置類相關@Import@ImportResource@PropertySource等註解引入的類進行遞歸,一次性全部引入。

由於文章篇幅有限不過多介紹了,這裏留點懸念,後面會出一篇文章專門介紹@Configuration註解,因爲它實在太太太重要了。

實現ImportSelector接口的類

這種引入方式需要實現ImportSelector接口:

public  class AImportSelector implements ImportSelector {

private static final String CLASS_NAME = "com.sue.cache.service.test13.A";

public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{CLASS_NAME};
}
}

@Import(AImportSelector.class)
@Configuration
public class TestConfiguration
{
}

這種方式的好處是selectImports方法返回的是數組,意味着可以同時引入多個類,還是非常方便的。

實現ImportBeanDefinitionRegistrar接口的類

這種引入方式需要實現ImportBeanDefinitionRegistrar接口:

public  class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(A.class);
registry.registerBeanDefinition("a", rootBeanDefinition);
}
}

@Import(AImportBeanDefinitionRegistrar.class)
@Configuration
public class TestConfiguration
{
}

這種方式是最靈活的,能在registerBeanDefinitions方法中獲取到BeanDefinitionRegistry容器註冊對象,可以手動控制BeanDefinition的創建和註冊。

當然@import註解非常人性化,還支持同時引入多種不同類型的類。

@Import({B.class,AImportBeanDefinitionRegistrar.class})
@Configuration
public class TestConfiguration
{
}

這四種引入類的方式各有千秋,總結如下:

  1. 普通類,用於創建沒有特殊要求的bean實例。
  2. @Configuration註解的配置類,用於層層嵌套引入的場景。
  3. 實現ImportSelector接口的類,用於一次性引入多個類的場景,或者可以根據不同的配置決定引入不同類的場景。
  4. 實現ImportBeanDefinitionRegistrar接口的類,主要用於可以手動控制BeanDefinition的創建和註冊的場景,它的方法中可以獲取BeanDefinitionRegistry註冊容器對象。

ConfigurationClassParser 類的 processImports 方法中可以看到這三種方式的處理邏輯:

最後的else方法其實包含了:普通類和@Configuration註解的配置類兩種不同的處理邏輯。

三. @ConfigurationProperties賦值

我們在項目中使用配置參數是非常常見的場景,比如,我們在配置線程池的時候,需要在applicationContext.propeties文件中定義如下配置:

thread.pool.corePoolSize=5
thread.pool.maxPoolSize=10
thread.pool.queueCapacity=200
thread.pool.keepAliveSeconds=30

方法一:通過@Value註解讀取這些配置。

public  class ThreadPoolConfig {

@Value("${thread.pool.corePoolSize:5}")
private int corePoolSize;

@Value("${thread.pool.maxPoolSize:10}")
private int maxPoolSize;

@Value("${thread.pool.queueCapacity:200}")
private int queueCapacity;

@Value("${thread.pool.keepAliveSeconds:30}")
private int keepAliveSeconds;

@Value("${thread.pool.threadNamePrefix:ASYNC_}")
private String threadNamePrefix;

@Bean
public Executor threadPoolExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

這種方式使用起來非常簡單,但建議在使用時都加上:,因爲:後面跟的是默認值,比如:@Value("${thread.pool.corePoolSize:5}"),定義的默認核心線程數是5。

假如有這樣的場景:business工程下定義了這個ThreadPoolConfig類,api工程引用了business工程,同時job工程也引用了business工程,而ThreadPoolConfig類只想在api工程中使用。這時,如果不配置默認值,job工程啓動的時候可能會報錯。

如果參數少還好,多的話,需要給每一個參數都加上@Value註解,是不是有點麻煩?

此外,還有一個問題,@Value註解定義的參數看起來有點分散,不容易辨別哪些參數是一組的。

這時,@ConfigurationProperties就派上用場了,它是springboot中新加的註解。

第一步,先定義ThreadPoolProperties類

@Data
@Component
@ConfigurationProperties("thread.pool")
public class ThreadPoolProperties {

private int corePoolSize;
private int maxPoolSize;
private int queueCapacity;
private int keepAliveSeconds;
private String threadNamePrefix;
}

第二步,使用ThreadPoolProperties類

@Configuration
public class ThreadPoolConfig {

@Autowired
private ThreadPoolProperties threadPoolProperties;

@Bean
public Executor threadPoolExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
executor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

使用@ConfigurationProperties註解,可以將thread.pool開頭的參數直接賦值到ThreadPoolProperties類的同名參數中,這樣省去了像@Value註解那樣一個個手動去對應的過程。

這種方式顯然要方便很多,我們只需編寫xxxProperties類,spring會自動裝配參數。此外,不同系列的參數可以定義不同的xxxProperties類,也便於管理,推薦優先使用這種方式。

它的底層是通過:ConfigurationPropertiesBindingPostProcessor類實現的,該類實現了BeanPostProcessor接口,在postProcessBeforeInitialization方法中解析@ConfigurationProperties註解,並且綁定數據到相應的對象上。

綁定是通過Binder類的bindObject方法完成的:

以上這段代碼會遞歸綁定數據,主要考慮了三種情況:

  • bindAggregate 綁定集合類
  • bindBean 綁定對象
  • bindProperty 綁定參數 前面兩種情況最終也會調用到bindProperty方法。

「此外,友情提醒一下:」

使用@ConfigurationProperties註解有些場景有問題,比如:在apollo中修改了某個參數,正常情況可以動態更新到@ConfigurationProperties註解定義的xxxProperties類的對象中,但是如果出現比較複雜的對象,比如:

private Map<String, Map<String,String>>  urls;

可能動態更新不了。

這時候該怎麼辦呢?

答案是使用ApolloConfigChangeListener監聽器自己處理:

@ConditionalOnClass(com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig.class)
public class ApolloConfigurationAutoRefresh implements ApplicationContextAware
{
private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

@ApolloConfigChangeListener
private void onChange(ConfigChangeEvent changeEvent{
refreshConfig(changeEvent.changedKeys()
)
;
}
private void refreshConfig(Set<String> changedKeys){
System.out.println("將變更的參數更新到相應的對象中");
}
}

四. spring事務要如何避坑?

spring中的事務功能主要分爲:聲明式事務編程式事務

聲明式事務

大多數情況下,我們在開發過程中使用更多的可能是聲明式事務,即使用@Transactional註解定義的事務,因爲它用起來更簡單,方便。

只需在需要執行的事務方法上,加上@Transactional註解就能自動開啓事務:

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

@Transactional
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}

這種聲明式事務之所以能生效,是因爲它的底層使用了AOP,創建了代理對象,調用TransactionInterceptor攔截器實現事務的功能。

spring事務有個特別的地方:它獲取的數據庫連接放在ThreadLocal中的,也就是說同一個線程中從始至終都能獲取同一個數據庫連接,可以保證同一個線程中多次數據庫操作在同一個事務中執行。

正常情況下是沒有問題的,但是如果使用不當,事務會失效,主要原因如下:

除了上述列舉的問題之外,由於@Transactional註解最小粒度是要被定義在方法上,如果有多層的事務方法調用,可能會造成大事務問題。

所以,建議在實際工作中少用@Transactional註解開啓事務。

編程式事務

一般情況下編程式事務我們可以通過TransactionTemplate類開啓事務功能。有個好消息,就是springboot已經默認實例化好這個對象了,我們能直接在項目中使用。

@Service
public class UserService {
@Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
transactionTemplate.execute((status) => {
doSameThing...
return Boolean.TRUE;
})
}
}

使用TransactionTemplate的編程式事務能避免很多事務失效的問題,但是對大事務問題,不一定能夠解決,只是說相對於使用@Transactional註解要好些。

五. 跨域問題的解決方案

關於跨域問題,前後端的解決方案還是挺多的,這裏我重點說說spring的解決方案,目前有三種:

一.使用@CrossOrigin註解

@RequestMapping("/user")
@RestController
public class UserController {

@CrossOrigin(origins = "http://localhost:8016")
@RequestMapping("/getUser")
public String getUser(@RequestParam("name") String name) {
System.out.println("name:" + name);
return "success";
}
}

該方案需要在跨域訪問的接口上加@CrossOrigin註解,訪問規則可以通過註解中的參數控制,控制粒度更細。如果需要跨域訪問的接口數量較少,可以使用該方案。

二.增加全局配置

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");

}
}

該方案需要實現WebMvcConfigurer接口,重寫addCorsMappings方法,在該方法中定義跨域訪問的規則。這是一個全局的配置,可以應用於所有接口。

三.自定義過濾器

@WebFilter("corsFilter")
@Configuration
public class CorsFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with");
chain.doFilter(request, response);
}

@Override
public void destroy() {

}
}

該方案通過在請求的header中增加Access-Control-Allow-Origin等參數解決跨域問題。

順便說一下,使用@CrossOrigin註解 和 實現WebMvcConfigurer接口的方案,spring在底層最終都會調用到DefaultCorsProcessor類的handleInternal方法:

最終三種方案殊途同歸,都會往header中添加跨域需要參數,只是實現形式不一樣而已。

六. 如何自定義starter

以前在沒有使用starter時,我們在項目中需要引入新功能,步驟一般是這樣的:

  • 在maven倉庫找該功能所需jar包
  • 在maven倉庫找該jar所依賴的其他jar包
  • 配置新功能所需參數

以上這種方式會帶來三個問題:

  1. 如果依賴包較多,找起來很麻煩,容易找錯,而且要花很多時間。
  2. 各依賴包之間可能會存在版本兼容性問題,項目引入這些jar包後,可能沒法正常啓動。
  3. 如果有些參數沒有配好,啓動服務也會報錯,沒有默認配置。

「爲了解決這些問題,springboot的starter機制應運而生」

starter機制帶來這些好處:

  1. 它能啓動相應的默認配置。
  2. 它能夠管理所需依賴,擺脫了需要到處找依賴 和 兼容性問題的困擾。
  3. 自動發現機制,將spring.factories文件中配置的類,自動注入到spring容器中。
  4. 遵循“約定大於配置”的理念。

在業務工程中只需引入starter包,就能使用它的功能,太爽了。

下面用一張圖,總結starter的幾個要素:

接下來我們一起實戰,定義一個自己的starter。

第一步,創建id-generate-starter工程:其中的pom.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<version>1.3.1</version>
<groupId>com.sue</groupId>
<artifactId>id-generate-spring-boot-starter</artifactId>
<name>id-generate-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>com.sue</groupId>
<artifactId>id-generate-spring-boot-autoconfigure</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
</project>

第二步,創建id-generate-spring-boot-autoconfigure工程:該項目當中包含:

  • pom.xml
  • spring.factories
  • IdGenerateAutoConfiguration
  • IdGenerateService
  • IdProperties pom.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<version>1.3.1</version>
<groupId>com.sue</groupId>
<artifactId>id-generate-spring-boot-autoconfigure</artifactId>
<name>id-generate-spring-boot-autoconfigure</name>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

spring.factories配置如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sue.IdGenerateAutoConfiguration

IdGenerateAutoConfiguration類:

@ConditionalOnClass(IdProperties.class)
@EnableConfigurationProperties(IdProperties.class)
@Configuration
public class IdGenerateAutoConfiguration
{

@Autowired
private IdProperties properties;

@Bean
public IdGenerateService idGenerateService() {
return new IdGenerateService(properties.getWorkId());
}
}

IdGenerateService類:

public  class IdGenerateService {

private Long workId;

public IdGenerateService(Long workId) {
this.workId = workId;
}

public Long generate() {
return new Random().nextInt(100) + this.workId;
}
}

IdProperties類:

@ConfigurationProperties(prefix = IdProperties.PREFIX)
public class IdProperties {


public static final String PREFIX = "sue";

private Long workId;

public Long getWorkId() {
return workId;
}

public void setWorkId(Long workId) {
this.workId = workId;
}
}

這樣在業務項目中引入相關依賴:

<dependency>
<groupId>com.sue</groupId>
<artifactId>id-generate-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>

就能使用注入使用IdGenerateService的功能了

  @Autowired
private IdGenerateService idGenerateService;

完美。

七.項目啓動時的附加功能

有時候我們需要在項目啓動時定製化一些附加功能,比如:加載一些系統參數、完成初始化、預熱本地緩存等,該怎麼辦呢?

好消息是springboot提供了:

  • CommandLineRunner
  • ApplicationRunner

這兩個接口幫助我們實現以上需求。

它們的用法還是挺簡單的,以ApplicationRunner接口爲例:

@Component
public class TestRunner implements ApplicationRunner {

@Autowired
private LoadDataService loadDataService;

public void run(ApplicationArguments args) throws Exception {
loadDataService.load();
}

}

實現ApplicationRunner接口,重寫run方法,在該方法中實現自己定製化需求。

如果項目中有多個類實現了ApplicationRunner接口,他們的執行順序要怎麼指定呢?

答案是使用@Order(n)註解,n的值越小越先執行。當然也可以通過@Priority註解指定順序。

springboot項目啓動時主要流程是這樣的:

SpringApplication類的callRunners方法中,我們能看到這兩個接口的具體調用:

最後還有一個問題:這兩個接口有什麼區別?

  • CommandLineRunner接口中run方法的參數爲String數組
  • ApplicationRunner中run方法的參數爲ApplicationArguments,該參數包含了String數組參數 和 一些可選參數。

嘮嘮家常

寫着寫着又有這麼多字了,按照慣例,爲了避免篇幅過長,今天就先寫到這裏。預告一下,後面會有AOP、BeanPostProcessor、Configuration註解等核心知識點的專題,每個主題的內容都挺多的,可以期待一下喔。

本文分享自微信公衆號 - 武培軒(wupeixuan404)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章