您好,登錄后才能下訂單哦!
本篇內容主要講解“Spring中的代碼技巧有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Spring中的代碼技巧有哪些”吧!
一. @Conditional的強大之處
不知道你們有沒有遇到過這些問題:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
某個功能需要根據項目中有沒有某個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方法中:
這個方法邏輯不復雜:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
先判斷有沒有使用Conditional注解,如果沒有直接返回false
收集condition到集合中
按order排序該集合
遍歷該集合,循環調用condition的matchs方法。
二. 如何妙用@Import?
有時我們需要在某個配置類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import注解完成這個功能。
如果你看過它的源碼會發現,引入的類支持三種不同類型。
但是我認為最好將普通類和@Configuration注解的配置類分開講解,所以列了四種不同類型:
普通類
這種引入方式是最簡單的,被引入的類會被實例化bean對象。
public class A { } @Import(A.class) @Configuration public class TestConfiguration { }
通過@Import注解引入A類,spring就能自動實例化A對象,然后在需要使用的地方通過@Autowired注解注入即可:
@Autowired ivate 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 { }
這四種引入類的方式各有千秋,總結如下:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
普通類,用于創建沒有特殊要求的bean實例。
@Configuration注解的配置類,用于層層嵌套引入的場景。
實現ImportSelector接口的類,用于一次性引入多個類的場景,或者可以根據不同的配置決定引入不同類的場景。
實現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包
配置新功能所需參數
以上這種方式會帶來三個問題:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
如果依賴包較多,找起來很麻煩,容易找錯,而且要花很多時間。
各依賴包之間可能會存在版本兼容性問題,項目引入這些jar包后,可能沒法正常啟動。
如果有些參數沒有配好,啟動服務也會報錯,沒有默認配置。
「為了解決這些問題,springboot的starter機制應運而生」。
starter機制帶來這些好處:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
它能啟動相應的默認配置。
它能夠管理所需依賴,擺脫了需要到處找依賴 和 兼容性問題的困擾。
自動發現機制,將spring.factories文件中配置的類,自動注入到spring容器中。
遵循“約定大于配置”的理念。
在業務工程中只需引入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數組參數 和 一些可選參數。
到此,相信大家對“Spring中的代碼技巧有哪些”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。