深入了解Spring中最常用的11個(gè)擴(kuò)展點(diǎn)
前言
我們一說到spring,可能第一個(gè)想到的是 IOC(控制反轉(zhuǎn)) 和 AOP(面向切面編程)。
沒錯(cuò),它們是spring的基石,得益于它們的優(yōu)秀設(shè)計(jì),使得spring能夠從眾多優(yōu)秀框架中脫穎而出。
除此之外,我們?cè)谑褂胹pring的過程中,有沒有發(fā)現(xiàn)它的擴(kuò)展能力非常強(qiáng)。由于這個(gè)優(yōu)勢(shì)的存在,讓spring擁有強(qiáng)大的包容能力,讓很多第三方應(yīng)用能夠輕松投入spring的懷抱。比如:rocketmq、mybatis、redis等。
今天跟大家一起聊聊,在Spring中最常用的11個(gè)擴(kuò)展點(diǎn)。

1.自定義攔截器
spring mvc攔截器根spring攔截器相比,它里面能夠獲取HttpServletRequest和HttpServletResponse等web對(duì)象實(shí)例。
spring mvc攔截器的頂層接口是:HandlerInterceptor,包含三個(gè)方法:
- preHandle 目標(biāo)方法執(zhí)行前執(zhí)行
- postHandle 目標(biāo)方法執(zhí)行后執(zhí)行
- afterCompletion 請(qǐng)求完成時(shí)執(zhí)行
為了方便我們一般情況會(huì)用HandlerInterceptor接口的實(shí)現(xiàn)類HandlerInterceptorAdapter類。
假如有權(quán)限認(rèn)證、日志、統(tǒng)計(jì)的場(chǎ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)限校驗(yàn)===");
return true;
}
}第二步,將該攔截器注冊(cè)到spring容器:
@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {
@Bean
public AuthInterceptor getAuthInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor());
}
}
第三步,在請(qǐng)求接口時(shí)spring mvc通過該攔截器,能夠自動(dòng)攔截該接口,并且校驗(yàn)權(quán)限。
2.獲取Spring容器對(duì)象
在我們?nèi)粘i_發(fā)中,經(jīng)常需要從Spring容器中獲取Bean,但你知道如何獲取Spring容器對(duì)象嗎?
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");
}
}
實(shí)現(xiàn)BeanFactoryAware接口,然后重寫setBeanFactory方法,就能從該方法中獲取到spring容器對(duì)象。
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");
}
}
實(shí)現(xiàn)ApplicationContextAware接口,然后重寫setApplicationContext方法,也能從該方法中獲取到spring容器對(duì)象。
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.全局異常處理
以前我們?cè)陂_發(fā)接口時(shí),如果出現(xiàn)異常,為了給用戶一個(gè)更友好的提示,例如:
@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping("/add")
public String add() {
int a = 10 / 0;
return "成功";
}
}
如果不做任何處理請(qǐng)求add接口結(jié)果直接報(bào)錯(cuò):

what?用戶能直接看到錯(cuò)誤信息?
這種交互方式給用戶的體驗(yàn)非常差,為了解決這個(gè)問題,我們通常會(huì)在接口中捕獲異常:
@GetMapping("/add")
public String add() {
String result = "成功";
try {
int a = 10 / 0;
} catch (Exception e) {
result = "數(shù)據(jù)異常";
}
return result;
}
接口改造后,出現(xiàn)異常時(shí)會(huì)提示:“數(shù)據(jù)異常”,對(duì)用戶來說更友好。
看起來挺不錯(cuò)的,但是有問題。。。
如果只是一個(gè)接口還好,但是如果項(xiàng)目中有成百上千個(gè)接口,都要加上異常捕獲代碼嗎?
答案是否定的,這時(shí)全局異常處理就派上用場(chǎng)了: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 "服務(wù)器內(nèi)部異常";
}
retur nnull;
}
}
只需在handleException方法中處理異常情況,業(yè)務(wù)接口中可以放心使用,不再需要捕獲異常(有人統(tǒng)一處理了)。真是爽歪歪。
4.類型轉(zhuǎn)換器
spring目前支持3中類型轉(zhuǎn)換器:
- Converter<S,T>:將 S 類型對(duì)象轉(zhuǎn)為 T 類型對(duì)象
- ConverterFactory<S, R>:將 S 類型對(duì)象轉(zhuǎn)為 R 類型及子類對(duì)象
- GenericConverter:它支持多個(gè)source和目標(biāo)類型的轉(zhuǎn)化,同時(shí)還提供了source和目標(biāo)類型的上下文,這個(gè)上下文能讓你實(shí)現(xiàn)基于屬性上的注解或信息來進(jìn)行類型轉(zhuǎn)換。
這3種類型轉(zhuǎn)換器使用的場(chǎng)景不一樣,我們以Converter<S,T>為例。假如:接口中接收參數(shù)的實(shí)體對(duì)象中,有個(gè)字段的類型是Date,但是實(shí)際傳參的是字符串類型:2021-01-03 10:20:15,要如何處理呢?
第一步,定義一個(gè)實(shí)體User:
@Data
public class User {
private Long id;
private String name;
private Date registerDate;
}
第二步,實(shí)現(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";
}
}
請(qǐng)求接口時(shí)User對(duì)象中registerDate字段會(huì)被自動(dòng)轉(zhuǎn)換成Date類型。
5.導(dǎo)入配置
有時(shí)我們需要在某個(gè)配置類中引入另外一些類,被引入的類也加到spring容器中。這時(shí)可以使用@Import注解完成這個(gè)功能。
如果你看過它的源碼會(huì)發(fā)現(xiàn),引入的類支持三種不同類型。
但是我認(rèn)為最好將普通類和@Configuration注解的配置類分開講解,所以列了四種不同類型:

5.1 普通類
這種引入方式是最簡單的,被引入的類會(huì)被實(shí)例化bean對(duì)象。
public class A {
}
@Import(A.class)
@Configuration
public class TestConfiguration {
}
通過@Import注解引入A類,spring就能自動(dòng)實(shí)例化A對(duì)象,然后在需要使用的地方通過@Autowired注解注入即可:
@Autowired private A a;
是不是挺讓人意外的?不用加@Bean注解也能實(shí)例化bean。
5.2 配置類
這種引入方式是最復(fù)雜的,因?yàn)?code>@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注解的配置類,會(huì)把該配置類相關(guān)@Import、@ImportResource、@PropertySource等注解引入的類進(jìn)行遞歸,一次性全部引入。
5.3 ImportSelector
這種引入方式需要實(shí)現(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ù)組,意味著可以同時(shí)引入多個(gè)類,還是非常方便的。
5.4 ImportBeanDefinitionRegistrar
這種引入方式需要實(shí)現(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容器注冊(cè)對(duì)象,可以手動(dòng)控制BeanDefinition的創(chuàng)建和注冊(cè)。
6.項(xiàng)目啟動(dòng)時(shí)
有時(shí)候我們需要在項(xiàng)目啟動(dòng)時(shí)定制化一些附加功能,比如:加載一些系統(tǒng)參數(shù)、完成初始化、預(yù)熱本地緩存等,該怎么辦呢?
好消息是springboot提供了:
- CommandLineRunner
- ApplicationRunner
這兩個(gè)接口幫助我們實(shí)現(xiàn)以上需求。
它們的用法還是挺簡單的,以ApplicationRunner接口為例:
@Component
public class TestRunner implements ApplicationRunner {
@Autowired
private LoadDataService loadDataService;
public void run(ApplicationArguments args) throws Exception {
loadDataService.load();
}
}
實(shí)現(xiàn)ApplicationRunner接口,重寫run方法,在該方法中實(shí)現(xiàn)自己定制化需求。
如果項(xiàng)目中有多個(gè)類實(shí)現(xiàn)了ApplicationRunner接口,他們的執(zhí)行順序要怎么指定呢?
答案是使用@Order(n)注解,n的值越小越先執(zhí)行。當(dāng)然也可以通過@Priority注解指定順序。
7.修改BeanDefinition
Spring IOC在實(shí)例化Bean對(duì)象之前,需要先讀取Bean的相關(guān)屬性,保存到BeanDefinition對(duì)象中,然后通過BeanDefinition對(duì)象,實(shí)例化Bean對(duì)象。
如果想修改BeanDefinition對(duì)象中的屬性,該怎么辦呢?
答:我們可以實(shí)現(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)對(duì)象,并且修改該對(duì)象的屬性。
8.初始化Bean前后
有時(shí),你想在初始化Bean前后,實(shí)現(xiàn)一些自己的邏輯。
這時(shí)可以實(shí)現(xiàn):BeanPostProcessor接口。
該接口目前有兩個(gè)方法:
- 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對(duì)象,則將它的userName設(shè)置成:蘇三說技術(shù)。
其實(shí),我們經(jīng)常使用的注解,比如:@Autowired、@Value、@Resource、@PostConstruct等,是通過AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor實(shí)現(xiàn)的。
9.初始化方法
目前spring中使用比較多的初始化bean的方法有:
- 使用@PostConstruct注解
- 實(shí)現(xiàn)InitializingBean接口
9.1 使用@PostConstruct注解
@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===初始化===");
}
}
在需要初始化的方法上增加@PostConstruct注解,這樣就有初始化的能力。
9.2 實(shí)現(xiàn)InitializingBean接口
@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===初始化===");
}
}
實(shí)現(xiàn)InitializingBean接口,重寫afterPropertiesSet方法,該方法中可以完成初始化功能。
10.關(guān)閉容器前
有時(shí)候,我們需要在關(guān)閉spring容器前,做一些額外的工作,比如:關(guān)閉資源文件等。
這時(shí)可以實(shí)現(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容器銷毀前,會(huì)調(diào)用該destroy方法,做一些額外的工作。
通常情況下,我們會(huì)同時(shí)實(shí)現(xiàn)InitializingBean和DisposableBean接口,重寫初始化方法和銷毀方法。
11.自定義作用域
我們都知道spring默認(rèn)支持的Scope只有兩種:
- singleton 單例,每次從spring容器中獲取到的bean都是同一個(gè)對(duì)象。
- prototype 多例,每次從spring容器中獲取到的bean都是不同的對(duì)象。
spring web又對(duì)Scope進(jìn)行了擴(kuò)展,增加了:
- RequestScope 同一次請(qǐng)求從spring容器中獲取到的bean都是同一個(gè)對(duì)象。
- SessionScope 同一個(gè)會(huì)話從spring容器中獲取到的bean都是同一個(gè)對(duì)象。
即便如此,有些場(chǎng)景還是無法滿足我們的要求。
比如,我們想在同一個(gè)線程中從spring容器獲取到的bean都是同一個(gè)對(duì)象,該怎么辦?
這就需要自定義Scope了。
第一步實(shí)現(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個(gè)擴(kuò)展點(diǎn)的詳細(xì)內(nèi)容,更多關(guān)于Spring擴(kuò)展點(diǎn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
圖文講解IDEA中根據(jù)數(shù)據(jù)庫自動(dòng)生成實(shí)體類
這篇文章主要以圖文講解IDEA中根據(jù)數(shù)據(jù)庫自動(dòng)生成實(shí)體類,本文主要以Mysql數(shù)據(jù)庫為例,應(yīng)該會(huì)對(duì)大家有所幫助,如果有錯(cuò)誤的地方,還望指正2023-03-03
解析Flink內(nèi)核原理與實(shí)現(xiàn)核心抽象
Flink API提供了開發(fā)的接口,此外,為了實(shí)現(xiàn)業(yè)務(wù)邏輯,還必須為開發(fā)者提供自定義業(yè)務(wù)邏輯的能力,下面為大家解析Flink內(nèi)核原理與實(shí)現(xiàn)核心抽象2021-08-08
基于Spring BeanUtils的copyProperties方法使用及注意事項(xiàng)
這篇文章主要介紹了基于Spring BeanUtils的copyProperties方法使用及注意事項(xiàng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
IDEA教程創(chuàng)建SpringBoot前后端分離項(xiàng)目示例圖解
在使用spring、mybatis等框架時(shí),配置文件很復(fù)雜,有時(shí)復(fù)雜的讓人想放棄Java,使用C#。springboot出現(xiàn)這一切問題就都不是問題2021-10-10
java實(shí)現(xiàn)字符串和日期類型相互轉(zhuǎn)換的方法
這篇文章主要介紹了java實(shí)現(xiàn)字符串和日期類型相互轉(zhuǎn)換的方法,涉及java針對(duì)日期與字符串的轉(zhuǎn)換與運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-02-02
解決@Cacheable在同一個(gè)類中方法調(diào)用不起作用的問題
這篇文章主要介紹了解決@Cacheable在同一個(gè)類中方法調(diào)用不起作用的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07

