深入了解Spring中最常用的11個擴展點
前言
我們一說到spring,可能第一個想到的是 IOC
(控制反轉(zhuǎn)) 和 AOP
(面向切面編程)。
沒錯,它們是spring的基石,得益于它們的優(yōu)秀設計,使得spring能夠從眾多優(yōu)秀框架中脫穎而出。
除此之外,我們在使用spring的過程中,有沒有發(fā)現(xiàn)它的擴展能力非常強。由于這個優(yōu)勢的存在,讓spring擁有強大的包容能力,讓很多第三方應用能夠輕松投入spring的懷抱。比如:rocketmq、mybatis、redis等。
今天跟大家一起聊聊,在Spring中最常用的11個擴展點。
1.自定義攔截器
spring mvc攔截器根spring攔截器相比,它里面能夠獲取HttpServletRequest
和HttpServletResponse
等web對象實例。
spring mvc攔截器的頂層接口是:HandlerInterceptor
,包含三個方法:
- preHandle 目標方法執(zhí)行前執(zhí)行
- postHandle 目標方法執(zhí)行后執(zhí)行
- afterCompletion 請求完成時執(zhí)行
為了方便我們一般情況會用HandlerInterceptor
接口的實現(xiàn)類HandlerInterceptorAdapter
類。
假如有權(quán)限認證、日志、統(tǒng)計的場景,可以使用該攔截器。
第一步,繼承HandlerInterceptorAdapter
類定義攔截器:
public class AuthInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestUrl = request.getRequestURI(); if (checkAuth(requestUrl)) { return true; } return false; } private boolean checkAuth(String requestUrl) { System.out.println("===權(quán)限校驗==="); return true; } }
第二步,將該攔截器注冊到spring容器:
@Configuration public class WebAuthConfig extends WebMvcConfigurerAdapter { @Bean public AuthInterceptor getAuthInterceptor() { return new AuthInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()); } }
第三步,在請求接口時spring mvc通過該攔截器,能夠自動攔截該接口,并且校驗權(quán)限。
2.獲取Spring容器對象
在我們?nèi)粘i_發(fā)中,經(jīng)常需要從Spring容器中獲取Bean,但你知道如何獲取Spring容器對象嗎?
2.1 BeanFactoryAware接口
@Service public class PersonService implements BeanFactoryAware { private BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } public void add() { Person person = (Person) beanFactory.getBean("person"); } }
實現(xiàn)BeanFactoryAware
接口,然后重寫setBeanFactory
方法,就能從該方法中獲取到spring容器對象。
2.2 ApplicationContextAware接口
@Service public class PersonService2 implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void add() { Person person = (Person) applicationContext.getBean("person"); } }
實現(xiàn)ApplicationContextAware
接口,然后重寫setApplicationContext
方法,也能從該方法中獲取到spring容器對象。
2.3 ApplicationListener接口
@Service public class PersonService3 implements ApplicationListener<ContextRefreshedEvent> { private ApplicationContext applicationContext; @Override public void onApplicationEvent(ContextRefreshedEvent event) { applicationContext = event.getApplicationContext(); } public void add() { Person person = (Person) applicationContext.getBean("person"); } }
3.全局異常處理
以前我們在開發(fā)接口時,如果出現(xiàn)異常,為了給用戶一個更友好的提示,例如:
@RequestMapping("/test") @RestController public class TestController { @GetMapping("/add") public String add() { int a = 10 / 0; return "成功"; } }
如果不做任何處理請求add接口結(jié)果直接報錯:
what?用戶能直接看到錯誤信息?
這種交互方式給用戶的體驗非常差,為了解決這個問題,我們通常會在接口中捕獲異常:
@GetMapping("/add") public String add() { String result = "成功"; try { int a = 10 / 0; } catch (Exception e) { result = "數(shù)據(jù)異常"; } return result; }
接口改造后,出現(xiàn)異常時會提示:“數(shù)據(jù)異常”,對用戶來說更友好。
看起來挺不錯的,但是有問題。。。
如果只是一個接口還好,但是如果項目中有成百上千個接口,都要加上異常捕獲代碼嗎?
答案是否定的,這時全局異常處理就派上用場了:RestControllerAdvice
。
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public String handleException(Exception e) { if (e instanceof ArithmeticException) { return "數(shù)據(jù)異常"; } if (e instanceof Exception) { return "服務器內(nèi)部異常"; } retur nnull; } }
只需在handleException
方法中處理異常情況,業(yè)務接口中可以放心使用,不再需要捕獲異常(有人統(tǒng)一處理了)。真是爽歪歪。
4.類型轉(zhuǎn)換器
spring目前支持3中類型轉(zhuǎn)換器:
- Converter<S,T>:將 S 類型對象轉(zhuǎn)為 T 類型對象
- ConverterFactory<S, R>:將 S 類型對象轉(zhuǎn)為 R 類型及子類對象
- GenericConverter:它支持多個source和目標類型的轉(zhuǎn)化,同時還提供了source和目標類型的上下文,這個上下文能讓你實現(xiàn)基于屬性上的注解或信息來進行類型轉(zhuǎn)換。
這3種類型轉(zhuǎn)換器使用的場景不一樣,我們以Converter<S,T>為
例。假如:接口中接收參數(shù)的實體對象中,有個字段的類型是Date,但是實際傳參的是字符串類型:2021-01-03 10:20:15,要如何處理呢?
第一步,定義一個實體User:
@Data public class User { private Long id; private String name; private Date registerDate; }
第二步,實現(xiàn)Converter
接口:
public class DateConverter implements Converter<String, Date> { private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override public Date convert(String source) { if (source != null && !"".equals(source)) { try { simpleDateFormat.parse(source); } catch (ParseException e) { e.printStackTrace(); } } return null; } }
第三步,將新定義的類型轉(zhuǎn)換器注入到spring容器中:
@Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new DateConverter()); } }
第四步,調(diào)用接口
@RequestMapping("/user") @RestController public class UserController { @RequestMapping("/save") public String save(@RequestBody User user) { return "success"; } }
請求接口時User對象中registerDate字段會被自動轉(zhuǎn)換成Date類型。
5.導入配置
有時我們需要在某個配置類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import
注解完成這個功能。
如果你看過它的源碼會發(fā)現(xiàn),引入的類支持三種不同類型。
但是我認為最好將普通類和@Configuration注解的配置類分開講解,所以列了四種不同類型:
5.1 普通類
這種引入方式是最簡單的,被引入的類會被實例化bean對象。
public class A { } @Import(A.class) @Configuration public class TestConfiguration { }
通過@Import
注解引入A類,spring就能自動實例化A對象,然后在需要使用的地方通過@Autowired
注解注入即可:
@Autowired private A a;
是不是挺讓人意外的?不用加@Bean
注解也能實例化bean。
5.2 配置類
這種引入方式是最復雜的,因為@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
等注解引入的類進行遞歸,一次性全部引入。
5.3 ImportSelector
這種引入方式需要實現(xiàn)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ù)組,意味著可以同時引入多個類,還是非常方便的。
5.4 ImportBeanDefinitionRegistrar
這種引入方式需要實現(xiàn)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)建和注冊。
6.項目啟動時
有時候我們需要在項目啟動時定制化一些附加功能,比如:加載一些系統(tǒng)參數(shù)、完成初始化、預熱本地緩存等,該怎么辦呢?
好消息是springboot提供了:
- CommandLineRunner
- ApplicationRunner
這兩個接口幫助我們實現(xiàn)以上需求。
它們的用法還是挺簡單的,以ApplicationRunner
接口為例:
@Component public class TestRunner implements ApplicationRunner { @Autowired private LoadDataService loadDataService; public void run(ApplicationArguments args) throws Exception { loadDataService.load(); } }
實現(xiàn)ApplicationRunner
接口,重寫run
方法,在該方法中實現(xiàn)自己定制化需求。
如果項目中有多個類實現(xiàn)了ApplicationRunner接口,他們的執(zhí)行順序要怎么指定呢?
答案是使用@Order(n)
注解,n的值越小越先執(zhí)行。當然也可以通過@Priority
注解指定順序。
7.修改BeanDefinition
Spring IOC在實例化Bean對象之前,需要先讀取Bean的相關(guān)屬性,保存到BeanDefinition
對象中,然后通過BeanDefinition對象,實例化Bean對象。
如果想修改BeanDefinition對象中的屬性,該怎么辦呢?
答:我們可以實現(xiàn)BeanFactoryPostProcessor
接口。
@Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory; BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class); beanDefinitionBuilder.addPropertyValue("id", 123); beanDefinitionBuilder.addPropertyValue("name", "蘇三說技術(shù)"); defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition()); } }
在postProcessBeanFactory方法中,可以獲取BeanDefinition的相關(guān)對象,并且修改該對象的屬性。
8.初始化Bean前后
有時,你想在初始化Bean前后,實現(xiàn)一些自己的邏輯。
這時可以實現(xiàn):BeanPostProcessor
接口。
該接口目前有兩個方法:
- postProcessBeforeInitialization 該在初始化方法之前調(diào)用。
- postProcessAfterInitialization 該方法再初始化方法之后調(diào)用。
例如:
@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof User) { ((User) bean).setUserName("蘇三說技術(shù)"); } return bean; } }
如果spring中存在User對象,則將它的userName設置成:蘇三說技術(shù)。
其實,我們經(jīng)常使用的注解,比如:@Autowired、@Value、@Resource、@PostConstruct等,是通過AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor實現(xiàn)的。
9.初始化方法
目前spring中使用比較多的初始化bean的方法有:
- 使用@PostConstruct注解
- 實現(xiàn)InitializingBean接口
9.1 使用@PostConstruct注解
@Service public class AService { @PostConstruct public void init() { System.out.println("===初始化==="); } }
在需要初始化的方法上增加@PostConstruct
注解,這樣就有初始化的能力。
9.2 實現(xiàn)InitializingBean接口
@Service public class BService implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { System.out.println("===初始化==="); } }
實現(xiàn)InitializingBean
接口,重寫afterPropertiesSet
方法,該方法中可以完成初始化功能。
10.關(guān)閉容器前
有時候,我們需要在關(guān)閉spring容器前,做一些額外的工作,比如:關(guān)閉資源文件等。
這時可以實現(xiàn)DisposableBean
接口,并且重寫它的destroy
方法:
@Service public class DService implements InitializingBean, DisposableBean { @Override public void destroy() throws Exception { System.out.println("DisposableBean destroy"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("InitializingBean afterPropertiesSet"); } }
這樣spring容器銷毀前,會調(diào)用該destroy方法,做一些額外的工作。
通常情況下,我們會同時實現(xiàn)InitializingBean和DisposableBean接口,重寫初始化方法和銷毀方法。
11.自定義作用域
我們都知道spring默認支持的Scope
只有兩種:
- singleton 單例,每次從spring容器中獲取到的bean都是同一個對象。
- prototype 多例,每次從spring容器中獲取到的bean都是不同的對象。
spring web又對Scope進行了擴展,增加了:
- RequestScope 同一次請求從spring容器中獲取到的bean都是同一個對象。
- SessionScope 同一個會話從spring容器中獲取到的bean都是同一個對象。
即便如此,有些場景還是無法滿足我們的要求。
比如,我們想在同一個線程中從spring容器獲取到的bean都是同一個對象,該怎么辦?
這就需要自定義Scope了。
第一步實現(xiàn)Scope接口:
public class ThreadLocalScope implements Scope { private static final ThreadLocal THREAD_LOCAL_SCOPE = new ThreadLocal(); @Override public Object get(String name, ObjectFactory<?> objectFactory) { Object value = THREAD_LOCAL_SCOPE.get(); if (value != null) { return value; } Object object = objectFactory.getObject(); THREAD_LOCAL_SCOPE.set(object); return object; } @Override public Object remove(String name) { THREAD_LOCAL_SCOPE.remove(); return null; } @Override public void registerDestructionCallback(String name, Runnable callback) { } @Override public Object resolveContextualObject(String key) { return null; } @Override public String getConversationId() { return null; } }
第二步將新定義的Scope注入到spring容器中:
@Component public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { beanFactory.registerScope("threadLocalScope", new ThreadLocalScope()); } }
第三步使用新定義的Scope:
@Scope("threadLocalScope") @Service public class CService { public void add() { } }
以上就是深入了解Spring中最常用的11個擴展點的詳細內(nèi)容,更多關(guān)于Spring擴展點的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java內(nèi)存結(jié)構(gòu)和數(shù)據(jù)類型
本文重點給大家介紹java內(nèi)存結(jié)構(gòu)和數(shù)據(jù)類型知識,非常不錯,具有參考借鑒價值,需要的朋友參考下2016-12-12Spring配置文件解析之BeanDefinitionParserDelegate詳解
這篇文章主要介紹了Spring配置文件解析之BeanDefinitionParserDelegate詳解,對于Spring的配置文件的解析處理操作是在BeanDefinitionParserDelegate中進行處理操作,接下來我們簡單介紹一下BeanDefinitionParserDelegate所做的處理操作,需要的朋友可以參考下2024-02-02SpringBoot多種環(huán)境自由切換的實現(xiàn)
本文主要介紹了SpringBoot多種環(huán)境自由切換的實現(xiàn),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08自從在 IDEA 中用了熱部署神器 JRebel 之后,開發(fā)效率提升了 10(真棒)
在javaweb開發(fā)過程中,使用熱部署神器 JRebel可以使class類還是更新spring配置文件都能立馬見到效率,本文給大家介紹JRebel的兩種安裝方法,小編建議使用第二種方法,具體安裝步驟跟隨小編一起看看吧2021-06-06springboot+redis+阿里云短信實現(xiàn)手機號登錄功能
這篇文章主要介紹了springboot+redis+阿里云短信實現(xiàn)手機號登錄功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2024-01-01