前言
上一篇文章《spring中這些能升華代碼的技巧,可能會讓你愛不釋手》發表之后,受到了不少讀者的好評,很多讀者都在期待續集,今天非常高興的通知大家,你們要的續集來了,本文繼續總結我認為spring中還不錯的知識點,希望對您有所幫助,
一. @Conditional的強大之處
不知道你們有沒有遇到過這些問題:
- 某個功能需要根據專案中有沒有某個jar判斷是否開啟該功能,
- 某個bean的實體化需要先判斷另一個bean有沒有實體化,再判斷是否實體化自己,
- 某個功能是否開啟,在組態檔中有個引數可以對它進行控制,
如果你有遇到過上述這些問題,那么恭喜你,本節內容非常適合你,
@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注解還有:ConditionalOnMissingClass、ConditionalOnMissingBean、ConditionalOnWebApplication等,
下面用一張圖整體認識一下@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.遍歷該集合,回圈呼叫condition的matchs方法,
二. 如何妙用@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時,我們在專案中需要引入新功能,步驟一般是這樣的:
1.在maven倉庫找該功能所需jar包
2.在maven倉庫找該jar所依賴的其他jar包
3.配置新功能所需引數
以上這種方式會帶來三個問題:
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陣列引數 和 一些可選引數,
最后說一句(求關注,別白嫖我)
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支持是我堅持寫作最大的動力,
求一鍵三連:點贊、轉發、在看,
關注公眾號:【蘇三說技術】,在公眾號中回復:面試、代碼神器、開發手冊、時間管理有超贊的粉絲福利,另外回復:加群,可以跟很多BAT大廠的前輩交流和學習,
個人公眾號

個人微信

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/282912.html
標籤:java
上一篇:Java進階(三)Java泛型理解,超級容易理解的 新手 教程!
下一篇:Java8新特性學習
