之前給大家寫過一篇 Bean 的生命周期,非常受歡迎,里面其實介紹了 Bean 生命周期中所有的擴展點。
今天給大家帶來的文章,可以作為 Spring 擴展點的補充,一共 11 個,工作中會經常用到,如果用得好,很可能會事半功倍哈。
前言
我們一說到spring,可能第一個想到的是 IOC
(控制反轉) 和 AOP
(面向切面編程)。
沒錯,它們是spring的基石,得益于它們的優秀設計,使得spring能夠從眾多優秀框架中脫穎而出。
除此之外,我們在使用spring的過程中,有沒有發現它的擴展能力非常強
。由于這個優勢的存在,讓spring擁有強大的包容能力,讓很多第三方應用能夠輕松投入spring的懷抱。比如:rocketmq、mybatis、redis等。
今天跟大家一起聊聊,在Spring中最常用的11個擴展點。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
1.自定義攔截器
spring mvc攔截器根spring攔截器相比,它里面能夠獲取HttpServletRequest
和HttpServletResponse
等web對象實例。
spring mvc攔截器的頂層接口是:HandlerInterceptor
,包含三個方法:
- preHandle 目標方法執行前執行
- postHandle 目標方法執行后執行
- afterCompletion 請求完成時執行
為了方便我們一般情況會用HandlerInterceptor
接口的實現類HandlerInterceptorAdapter
類。
假如有權限認證、日志、統計的場景,可以使用該攔截器。
第一步,繼承HandlerInterceptorAdapter
類定義攔截器:
publicclassAuthInterceptorextendsHandlerInterceptorAdapter{
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)
throwsException{
StringrequestUrl=request.getRequestURI();
if(checkAuth(requestUrl)){
returntrue;
}
returnfalse;
}
privatebooleancheckAuth(StringrequestUrl){
System.out.println("===權限校驗===");
returntrue;
}
}
第二步,將該攔截器注冊到spring容器:
@Configuration
publicclassWebAuthConfigextendsWebMvcConfigurerAdapter{
@Bean
publicAuthInterceptorgetAuthInterceptor(){
returnnewAuthInterceptor();
}
@Override
publicvoidaddInterceptors(InterceptorRegistryregistry){
registry.addInterceptor(newAuthInterceptor());
}
}
第三步,在請求接口時spring mvc通過該攔截器,能夠自動攔截該接口,并且校驗權限。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
2.獲取Spring容器對象
在我們日常開發中,經常需要從Spring容器中獲取Bean,但你知道如何獲取Spring容器對象嗎?
2.1 BeanFactoryAware接口
@Service
publicclassPersonServiceimplementsBeanFactoryAware{
privateBeanFactorybeanFactory;
@Override
publicvoidsetBeanFactory(BeanFactorybeanFactory)throwsBeansException{
this.beanFactory=beanFactory;
}
publicvoidadd(){
Personperson=(Person)beanFactory.getBean("person");
}
}
實現BeanFactoryAware
接口,然后重寫setBeanFactory
方法,就能從該方法中獲取到spring容器對象。
2.2 ApplicationContextAware接口
@Service
publicclassPersonService2implementsApplicationContextAware{
privateApplicationContextapplicationContext;
@Override
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
this.applicationContext=applicationContext;
}
publicvoidadd(){
Personperson=(Person)applicationContext.getBean("person");
}
}
實現ApplicationContextAware
接口,然后重寫setApplicationContext
方法,也能從該方法中獲取到spring容器對象。
2.3 ApplicationListener接口
@Service
publicclassPersonService3implementsApplicationListener<ContextRefreshedEvent>{
privateApplicationContextapplicationContext;
@Override
publicvoidonApplicationEvent(ContextRefreshedEventevent){
applicationContext=event.getApplicationContext();
}
publicvoidadd(){
Personperson=(Person)applicationContext.getBean("person");
}
}
3.全局異常處理
以前我們在開發接口時,如果出現異常,為了給用戶一個更友好的提示,例如:
@RequestMapping("/test")
@RestController
publicclassTestController{
@GetMapping("/add")
publicStringadd(){
inta=10/0;
return"成功";
}
}
如果不做任何處理請求add接口結果直接報錯:
what?用戶能直接看到錯誤信息?
這種交互方式給用戶的體驗非常差,為了解決這個問題,我們通常會在接口中捕獲異常:
@GetMapping("/add")
publicStringadd(){
Stringresult="成功";
try{
inta=10/0;
}catch(Exceptione){
result="數據異常";
}
returnresult;
}
接口改造后,出現異常時會提示:“數據異常”,對用戶來說更友好。
看起來挺不錯的,但是有問題。。。
如果只是一個接口還好,但是如果項目中有成百上千個接口,都要加上異常捕獲代碼嗎?
答案是否定的,這時全局異常處理就派上用場了:RestControllerAdvice
。
@RestControllerAdvice
publicclassGlobalExceptionHandler{
@ExceptionHandler(Exception.class)
publicStringhandleException(Exceptione){
if(einstanceofArithmeticException){
return"數據異常";
}
if(einstanceofException){
return"服務器內部異常";
}
returnnull;
}
}
只需在handleException
方法中處理異常情況,業務接口中可以放心使用,不再需要捕獲異常(有人統一處理了)。真是爽歪歪。
4.類型轉換器
spring目前支持3中類型轉換器:
-
Converter
:將 S 類型對象轉為 T 類型對象 -
ConverterFactory
:將 S 類型對象轉為 R 類型及子類對象 - GenericConverter:它支持多個source和目標類型的轉化,同時還提供了source和目標類型的上下文,這個上下文能讓你實現基于屬性上的注解或信息來進行類型轉換。
這3種類型轉換器使用的場景不一樣,我們以Converter
例。假如:接口中接收參數的實體對象中,有個字段的類型是Date,但是實際傳參的是字符串類型:2021-01-03 1015,要如何處理呢?為
第一步,定義一個實體User:
@Data
publicclassUser{
privateLongid;
privateStringname;
privateDateregisterDate;
}
第二步,實現Converter
接口:
publicclassDateConverterimplementsConverter<String,Date>{
privateSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-ddHHss");
@Override
publicDateconvert(Stringsource){
if(source!=null&&!"".equals(source)){
try{
simpleDateFormat.parse(source);
}catch(ParseExceptione){
e.printStackTrace();
}
}
returnnull;
}
}
第三步,將新定義的類型轉換器注入到spring容器中:
@Configuration
publicclassWebConfigextendsWebMvcConfigurerAdapter{
@Override
publicvoidaddFormatters(FormatterRegistryregistry){
registry.addConverter(newDateConverter());
}
}
第四步,調用接口
@RequestMapping("/user")
@RestController
publicclassUserController{
@RequestMapping("/save")
publicStringsave(@RequestBodyUseruser){
return"success";
}
}
請求接口時User對象中registerDate字段會被自動轉換成Date類型。
5.導入配置
有時我們需要在某個配置類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import
注解完成這個功能。
如果你看過它的源碼會發現,引入的類支持三種不同類型。
但是我認為最好將普通類和@Configuration注解的配置類分開講解,所以列了四種不同類型:
5.1 普通類
這種引入方式是最簡單的,被引入的類會被實例化bean對象。
publicclassA{
}
@Import(A.class)
@Configuration
publicclassTestConfiguration{
}
通過@Import
注解引入A類,spring就能自動實例化A對象,然后在需要使用的地方通過@Autowired
注解注入即可:
@Autowired
privateAa;
是不是挺讓人意外的?不用加@Bean
注解也能實例化bean。
5.2 配置類
這種引入方式是最復雜的,因為@Configuration
注解還支持多種組合注解,比如:
- @Import
- @ImportResource
- @PropertySource等。
publicclassA{
}
publicclassB{
}
@Import(B.class)
@Configuration
publicclassAConfiguration{
@Bean
publicAa(){
returnnewA();
}
}
@Import(AConfiguration.class)
@Configuration
publicclassTestConfiguration{
}
通過@Import注解引入@Configuration注解的配置類,會把該配置類相關@Import
、@ImportResource
、@PropertySource
等注解引入的類進行遞歸,一次性全部引入。
5.3 ImportSelector
這種引入方式需要實現ImportSelector
接口:
publicclassAImportSelectorimplementsImportSelector{
privatestaticfinalStringCLASS_NAME="com.sue.cache.service.test13.A";
publicString[]selectImports(AnnotationMetadataimportingClassMetadata){
returnnewString[]{CLASS_NAME};
}
}
@Import(AImportSelector.class)
@Configuration
publicclassTestConfiguration{
}
這種方式的好處是selectImports
方法返回的是數組,意味著可以同時引入多個類,還是非常方便的。
5.4 ImportBeanDefinitionRegistrar
這種引入方式需要實現ImportBeanDefinitionRegistrar
接口:
publicclassAImportBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar{
@Override
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
RootBeanDefinitionrootBeanDefinition=newRootBeanDefinition(A.class);
registry.registerBeanDefinition("a",rootBeanDefinition);
}
}
@Import(AImportBeanDefinitionRegistrar.class)
@Configuration
publicclassTestConfiguration{
}
這種方式是最靈活的,能在registerBeanDefinitions
方法中獲取到BeanDefinitionRegistry
容器注冊對象,可以手動控制BeanDefinition的創建和注冊。
6.項目啟動時
有時候我們需要在項目啟動時定制化一些附加功能,比如:加載一些系統參數、完成初始化、預熱本地緩存等,該怎么辦呢?
好消息是springboot提供了:
- CommandLineRunner
- ApplicationRunner
這兩個接口幫助我們實現以上需求。
它們的用法還是挺簡單的,以ApplicationRunner
接口為例:
@Component
publicclassTestRunnerimplementsApplicationRunner{
@Autowired
privateLoadDataServiceloadDataService;
publicvoidrun(ApplicationArgumentsargs)throwsException{
loadDataService.load();
}
}
實現ApplicationRunner
接口,重寫run
方法,在該方法中實現自己定制化需求。
如果項目中有多個類實現了ApplicationRunner接口,他們的執行順序要怎么指定呢?
答案是使用@Order(n)
注解,n的值越小越先執行。當然也可以通過@Priority
注解指定順序。
7.修改BeanDefinition
Spring IOC在實例化Bean對象之前,需要先讀取Bean的相關屬性,保存到BeanDefinition
對象中,然后通過BeanDefinition對象,實例化Bean對象。
如果想修改BeanDefinition對象中的屬性,該怎么辦呢?
答:我們可以實現BeanFactoryPostProcessor
接口。
@Component
publicclassMyBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{
@Override
publicvoidpostProcessBeanFactory(ConfigurableListableBeanFactoryconfigurableListableBeanFactory)throwsBeansException{
DefaultListableBeanFactorydefaultListableBeanFactory=(DefaultListableBeanFactory)configurableListableBeanFactory;
BeanDefinitionBuilderbeanDefinitionBuilder=BeanDefinitionBuilder.genericBeanDefinition(User.class);
beanDefinitionBuilder.addPropertyValue("id",123);
beanDefinitionBuilder.addPropertyValue("name","蘇三說技術");
defaultListableBeanFactory.registerBeanDefinition("user",beanDefinitionBuilder.getBeanDefinition());
}
}
在postProcessBeanFactory方法中,可以獲取BeanDefinition的相關對象,并且修改該對象的屬性。
8.初始化Bean前后
有時,你想在初始化Bean前后,實現一些自己的邏輯。
這時可以實現:BeanPostProcessor
接口。
該接口目前有兩個方法:
- postProcessBeforeInitialization 該在初始化方法之前調用。
- postProcessAfterInitialization 該方法再初始化方法之后調用。
例如:
@Component
publicclassMyBeanPostProcessorimplementsBeanPostProcessor{
@Override
publicObjectpostProcessAfterInitialization(Objectbean,StringbeanName)throwsBeansException{
if(beaninstanceofUser){
((User)bean).setUserName("蘇三說技術");
}
returnbean;
}
}
如果spring中存在User對象,則將它的userName設置成:蘇三說技術。
其實,我們經常使用的注解,比如:@Autowired、@Value、@Resource、@PostConstruct等,是通過AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor實現的。
9.初始化方法
目前spring中使用比較多的初始化bean的方法有:
- 使用@PostConstruct注解
- 實現InitializingBean接口
9.1 使用@PostConstruct注解
@Service
publicclassAService{
@PostConstruct
publicvoidinit(){
System.out.println("===初始化===");
}
}
在需要初始化的方法上增加@PostConstruct
注解,這樣就有初始化的能力。
9.2 實現InitializingBean接口
@Service
publicclassBServiceimplementsInitializingBean{
@Override
publicvoidafterPropertiesSet()throwsException{
System.out.println("===初始化===");
}
}
實現InitializingBean
接口,重寫afterPropertiesSet
方法,該方法中可以完成初始化功能。
10.關閉容器前
有時候,我們需要在關閉spring容器前,做一些額外的工作,比如:關閉資源文件等。
這時可以實現DisposableBean
接口,并且重寫它的destroy
方法:
@Service
publicclassDServiceimplementsInitializingBean,DisposableBean{
@Override
publicvoiddestroy()throwsException{
System.out.println("DisposableBeandestroy");
}
@Override
publicvoidafterPropertiesSet()throwsException{
System.out.println("InitializingBeanafterPropertiesSet");
}
}
這樣spring容器銷毀前,會調用該destroy方法,做一些額外的工作。
通常情況下,我們會同時實現InitializingBean和DisposableBean接口,重寫初始化方法和銷毀方法。
11.自定義作用域
我們都知道spring默認支持的Scope
只有兩種:
- singleton 單例,每次從spring容器中獲取到的bean都是同一個對象。
- prototype 多例,每次從spring容器中獲取到的bean都是不同的對象。
spring web又對Scope進行了擴展,增加了:
- RequestScope 同一次請求從spring容器中獲取到的bean都是同一個對象。
- SessionScope 同一個會話從spring容器中獲取到的bean都是同一個對象。
即便如此,有些場景還是無法滿足我們的要求。
比如,我們想在同一個線程中從spring容器獲取到的bean都是同一個對象,該怎么辦?
這就需要自定義Scope了。
第一步實現Scope接口:
publicclassThreadLocalScopeimplementsScope{
privatestaticfinalThreadLocalTHREAD_LOCAL_SCOPE=newThreadLocal();
@Override
publicObjectget(Stringname,ObjectFactory>objectFactory){
Objectvalue=THREAD_LOCAL_SCOPE.get();
if(value!=null){
returnvalue;
}
Objectobject=objectFactory.getObject();
THREAD_LOCAL_SCOPE.set(object);
returnobject;
}
@Override
publicObjectremove(Stringname){
THREAD_LOCAL_SCOPE.remove();
returnnull;
}
@Override
publicvoidregisterDestructionCallback(Stringname,Runnablecallback){
}
@Override
publicObjectresolveContextualObject(Stringkey){
returnnull;
}
@Override
publicStringgetConversationId(){
returnnull;
}
}
第二步將新定義的Scope注入到spring容器中:
@Component
publicclassThreadLocalBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{
@Override
publicvoidpostProcessBeanFactory(ConfigurableListableBeanFactorybeanFactory)throwsBeansException{
beanFactory.registerScope("threadLocalScope",newThreadLocalScope());
}
}
第三步使用新定義的Scope:
@Scope("threadLocalScope")
@Service
publicclassCService{
publicvoidadd(){
}
}
審核編輯 :李倩
-
框架
+關注
關注
0文章
403瀏覽量
17487 -
容器
+關注
關注
0文章
495瀏覽量
22061 -
spring
+關注
關注
0文章
340瀏覽量
14343
原文標題:聊聊 Spring 中最常用的 11 個擴展點
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論