spring中那些讓你愛不釋手的代碼技巧
一. @Conditional的強(qiáng)大之處
不知道你們有沒有遇到過這些問題:
某個功能需要根據(jù)項目中有沒有某個jar判斷是否開啟該功能。
某個bean的實例化需要先判斷另一個bean有沒有實例化,再判斷是否實例化自己。
某個功能是否開啟,在配置文件中有個參數(shù)可以對它進(jìn)行控制。
如果你有遇到過上述這些問題,那么恭喜你,本節(jié)內(nèi)容非常適合你。
@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嗎?怎么現(xiàn)在判斷某個類了?
?直接判斷有沒有該jar下的某個關(guān)鍵類更簡單。?
這個注解有個升級版的應(yīng)用場景:比如common工程中寫了一個發(fā)消息的工具類mqTemplate,業(yè)務(wù)工程引用了common工程,只需再引入消息中間件,比如rocketmq的jar包,就能開啟mqTemplate的功能。而如果有另一個業(yè)務(wù)工程,通用引用了common工程,如果不需要發(fā)消息的功能,不引入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文件中配置參數(shù):
demo.enable=false
各參數(shù)含義:
prefix 表示參數(shù)名的前綴,這里是demo
name 表示參數(shù)名
havingValue 表示指定的值,參數(shù)中配置的值需要跟指定的值比較是否相等,相等才滿足條件
matchIfMissing 表示是否允許缺省配置。
這個功能可以作為開關(guān),相比EnableXXX注解的開關(guān)更優(yōu)雅,因為它可以通過參數(shù)配置是否開啟,而EnableXXX注解的開關(guān)需要在代碼中硬編碼開啟或關(guān)閉。
其他的Conditional注解
當(dāng)然,spring用得比較多的Conditional注解還有:ConditionalOnMissingClass、ConditionalOnMissingBean、ConditionalOnWebApplication等。
下面用一張圖整體認(rèn)識一下@Conditional家族。

自定義Conditional
說實話,個人認(rèn)為springboot自帶的Conditional系列已經(jīng)可以滿足我們絕大多數(shù)的需求了。但如果你有比較特殊的場景,也可以自定義自定義Conditional。
第一步,自定義注解:
@Conditional(MyCondition.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface MyConditionOnProperty {
String name() default "";
String havingValue() default "";
}
第二步,實現(xiàn)Condition接口:
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println("實現(xiàn)自定義邏輯");
return false;
}
}
第三步,使用@MyConditionOnProperty注解。
Conditional的奧秘就藏在ConfigurationClassParser類的processConfigurationClass方法中:

這個方法邏輯不復(fù)雜:

先判斷有沒有使用Conditional注解,如果沒有直接返回false 收集condition到集合中 按 order排序該集合遍歷該集合,循環(huán)調(diào)用 condition的matchs方法。
二. 如何妙用@Import?
@Import注解完成這個功能。
普通類
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注解的配置類,會把該配置類相關(guān)@Import、@ImportResource、@PropertySource等注解引入的類進(jìn)行遞歸,一次性全部引入。@Configuration注解,因為它實在太太太重要了。實現(xiàn)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方法返回的是數(shù)組,意味著可以同時引入多個類,還是非常方便的。實現(xiàn)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的創(chuàng)建和注冊。@import注解非常人性化,還支持同時引入多種不同類型的類。@Import({B.class,AImportBeanDefinitionRegistrar.class})
@Configuration
public class TestConfiguration {
}
普通類,用于創(chuàng)建沒有特殊要求的bean實例。 @Configuration注解的配置類,用于層層嵌套引入的場景。 實現(xiàn)ImportSelector接口的類,用于一次性引入多個類的場景,或者可以根據(jù)不同的配置決定引入不同類的場景。 實現(xiàn)ImportBeanDefinitionRegistrar接口的類,主要用于可以手動控制BeanDefinition的創(chuàng)建和注冊的場景,它的方法中可以獲取BeanDefinitionRegistry注冊容器對象。
ConfigurationClassParser類的processImports方法中可以看到這三種方式的處理邏輯:
三. @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;
}
}
:,因為:后面跟的是默認(rèn)值,比如:@Value("${thread.pool.corePoolSize:5}"),定義的默認(rèn)核心線程數(shù)是5。@Value注解,是不是有點麻煩?@Value注解定義的參數(shù)看起來有點分散,不容易辨別哪些參數(shù)是一組的。@ConfigurationProperties就派上用場了,它是springboot中新加的注解。@Data
@Component
@ConfigurationProperties("thread.pool")
public class ThreadPoolProperties {
private int corePoolSize;
private int maxPoolSize;
private int queueCapacity;
private int keepAliveSeconds;
private String threadNamePrefix;
}
@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開頭的參數(shù)直接賦值到ThreadPoolProperties類的同名參數(shù)中,這樣省去了像@Value注解那樣一個個手動去對應(yīng)的過程。ConfigurationPropertiesBindingPostProcessor類實現(xiàn)的,該類實現(xiàn)了BeanPostProcessor接口,在postProcessBeforeInitialization方法中解析@ConfigurationProperties注解,并且綁定數(shù)據(jù)到相應(yīng)的對象上。Binder類的bindObject方法完成的:
bindAggregate綁定集合類bindBean綁定對象bindProperty綁定參數(shù) 前面兩種情況最終也會調(diào)用到bindProperty方法。
@ConfigurationProperties注解有些場景有問題,比如:在apollo中修改了某個參數(shù),正常情況可以動態(tài)更新到@ConfigurationProperties注解定義的xxxProperties類的對象中,但是如果出現(xiàn)比較復(fù)雜的對象,比如:private Map<String, Map<String,String>> urls;
ApolloConfigChangeListener監(jiān)聽器自己處理:@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("將變更的參數(shù)更新到相應(yīng)的對象中");
}
}
四. spring事務(wù)要如何避坑?
聲明式事務(wù)和編程式事務(wù)。聲明式事務(wù)
聲明式事務(wù),即使用@Transactional注解定義的事務(wù),因為它用起來更簡單,方便。@Transactional注解就能自動開啟事務(wù):@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
TransactionInterceptor攔截器實現(xiàn)事務(wù)的功能。ThreadLocal中的,也就是說同一個線程中從始至終都能獲取同一個數(shù)據(jù)庫連接,可以保證同一個線程中多次數(shù)據(jù)庫操作在同一個事務(wù)中執(zhí)行。?
@Transactional注解最小粒度是要被定義在方法上,如果有多層的事務(wù)方法調(diào)用,可能會造成大事務(wù)問題。
@Transactional注解開啟事務(wù)。編程式事務(wù)
TransactionTemplate類開啟事務(wù)功能。有個好消息,就是springboot已經(jīng)默認(rèn)實例化好這個對象了,我們能直接在項目中使用。@Service
public class UserService {
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
doSameThing...
return Boolean.TRUE;
})
}
}
TransactionTemplate的編程式事務(wù)能避免很多事務(wù)失效的問題,但是對大事務(wù)問題,不一定能夠解決,只是說相對于使用@Transactional注解要好些。五. 跨域問題的解決方案
一.使用@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注解,訪問規(guī)則可以通過注解中的參數(shù)控制,控制粒度更細(xì)。如果需要跨域訪問的接口數(shù)量較少,可以使用該方案。二.增加全局配置
@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方法,在該方法中定義跨域訪問的規(guī)則。這是一個全局的配置,可以應(yīng)用于所有接口。三.自定義過濾器
@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等參數(shù)解決跨域問題。@CrossOrigin注解 和 實現(xiàn)WebMvcConfigurer接口的方案,spring在底層最終都會調(diào)用到DefaultCorsProcessor類的handleInternal方法:
header中添加跨域需要參數(shù),只是實現(xiàn)形式不一樣而已。六. 如何自定義starter
starter時,我們在項目中需要引入新功能,步驟一般是這樣的:在maven倉庫找該功能所需jar包 在maven倉庫找該jar所依賴的其他jar包 配置新功能所需參數(shù)
如果依賴包較多,找起來很麻煩,容易找錯,而且要花很多時間。 各依賴包之間可能會存在版本兼容性問題,項目引入這些jar包后,可能沒法正常啟動。 如果有些參數(shù)沒有配好,啟動服務(wù)也會報錯,沒有默認(rèn)配置。
starter機(jī)制應(yīng)運(yùn)而生」。它能啟動相應(yīng)的默認(rèn)配置。 它能夠管理所需依賴,擺脫了需要到處找依賴 和 兼容性問題的困擾。 自動發(fā)現(xiàn)機(jī)制,將spring.factories文件中配置的類,自動注入到spring容器中。 遵循“約定大于配置”的理念。


<?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>

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>
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sue.IdGenerateAutoConfiguration
@ConditionalOnClass(IdProperties.class)
@EnableConfigurationProperties(IdProperties.class)
@Configuration
public class IdGenerateAutoConfiguration {
@Autowired
private IdProperties properties;
@Bean
public IdGenerateService idGenerateService() {
return new IdGenerateService(properties.getWorkId());
}
}
public class IdGenerateService {
private Long workId;
public IdGenerateService(Long workId) {
this.workId = workId;
}
public Long generate() {
return new Random().nextInt(100) + this.workId;
}
}
@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>
@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方法,在該方法中實現(xiàn)自己定制化需求。ApplicationRunner接口,他們的執(zhí)行順序要怎么指定呢?@Order(n)注解,n的值越小越先執(zhí)行。當(dāng)然也可以通過@Priority注解指定順序。
SpringApplication類的callRunners方法中,我們能看到這兩個接口的具體調(diào)用:
CommandLineRunner接口中run方法的參數(shù)為String數(shù)組 ApplicationRunner中run方法的參數(shù)為ApplicationArguments,該參數(shù)包含了String數(shù)組參數(shù) 和 一些可選參數(shù)。
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
