今天給大家帶來的文章,可以作為 Spring 擴充點的補充,一共 11 個,工作中會經常用到,如果用得好,很可能會事半功倍哈。
前言
我們一說到spring,可能第一個想到的是 IOC(控制反轉) 和 AOP(面向切面程式設計)。
沒錯,它們是spring的基石,得益于它們的優秀設計,使得spring能夠從衆多優秀架構中脫穎而出。
除此之外,我們在使用spring的過程中,有沒有發現它的擴充能力非常強。由于這個優勢的存在,讓spring擁有強大的包容能力,讓很多第三方應用能夠輕松投入spring的懷抱。比如:rocketmq、mybatis、redis等。
今天跟大家一起聊聊,在Spring中最常用的11個擴充點。
1.自定義攔截器
spring mvc攔截器根spring攔截器相比,它裡面能夠擷取HttpServletRequest和HttpServletResponse等web對象執行個體。
spring mvc攔截器的頂層接口是:HandlerInterceptor,包含三個方法:
- preHandle 目标方法執行前執行
- postHandle 目标方法執行後執行
- afterCompletion 請求完成時執行
為了友善我們一般情況會用HandlerInterceptor接口的實作類HandlerInterceptorAdapter類。
假如有權限認證、日志、統計的場景,可以使用該攔截器。
第一步,繼承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("===權限校驗===");
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通過該攔截器,能夠自動攔截該接口,并且校驗權限。
2.擷取Spring容器對象
在我們日常開發中,經常需要從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");
}
}
實作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");
}
}
實作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.全局異常處理
以前我們在開發接口時,如果出現異常,為了給使用者一個更友好的提示,例如:
@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping("/add")
public String add() {
int a = 10 / 0;
return "成功";
}
}
如果不做任何處理請求add接口結果直接報錯:
what?使用者能直接看到錯誤資訊?
這種互動方式給使用者的體驗非常差,為了解決這個問題,我們通常會在接口中捕獲異常:
@GetMapping("/add")
public String add() {
String result = "成功";
try {
int a = 10 / 0;
} catch (Exception e) {
result = "資料異常";
}
return result;
}
接口改造後,出現異常時會提示:“資料異常”,對使用者來說更友好。
看起來挺不錯的,但是有問題。。。
如果隻是一個接口還好,但是如果項目中有成百上千個接口,都要加上異常捕獲代碼嗎?
答案是否定的,這時全局異常處理就派上用場了:RestControllerAdvice。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
if (e instanceof ArithmeticException) {
return "資料異常";
}
if (e instanceof Exception) {
return "伺服器内部異常";
}
retur nnull;
}
}
隻需在handleException方法中處理異常情況,業務接口中可以放心使用,不再需要捕獲異常(有人統一處理了)。真是爽歪歪。
4.類型轉換器
spring目前支援3中類型轉換器:
- Converter<S,T>:将 S 類型對象轉為 T 類型對象
- ConverterFactory<S, R>:将 S 類型對象轉為 R 類型及子類對象
- GenericConverter:它支援多個source和目标類型的轉化,同時還提供了source和目标類型的上下文,這個上下文能讓你實作基于屬性上的注解或資訊來進行類型轉換。
這3種類型轉換器使用的場景不一樣,我們以Converter<S,T>為例。假如:接口中接收參數的實體對象中,有個字段的類型是Date,但是實際傳參的是字元串類型:2021-01-03 10:20:15,要如何處理呢?
第一步,定義一個實體User:
@Data
public class User {
private Long id;
private String name;
private Date registerDate;
}
第二步,實作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;
}
}
第三步,将新定義的類型轉換器注入到spring容器中:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new DateConverter());
}
}
第四步,調用接口
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/save")
public String save(@RequestBody User user) {
return "success";
}
}
請求接口時User對象中registerDate字段會被自動轉換成Date類型。
5.導入配置
有時我們需要在某個配置類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import注解完成這個功能。
如果你看過它的源碼會發現,引入的類支援三種不同類型。
但是我認為最好将普通類和@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注解的配置類,會把該配置類相關@Import、@ImportResource、@PropertySource等注解引入的類進行遞歸,一次性全部引入。
5.3 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方法傳回的是數組,意味着可以同時引入多個類,還是非常友善的。
5.4 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的建立和注冊。
6.項目啟動時
有時候我們需要在項目啟動時定制化一些附加功能,比如:加載一些系統參數、完成初始化、預熱本地緩存等,該怎麼辦呢?
好消息是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注解指定順序。
7.修改BeanDefinition
Spring IOC在執行個體化Bean對象之前,需要先讀取Bean的相關屬性,儲存到BeanDefinition對象中,然後通過BeanDefinition對象,執行個體化Bean對象。
如果想修改BeanDefinition對象中的屬性,該怎麼辦呢?
答:我們可以實作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", "蘇三說技術");
defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
}
}
在postProcessBeanFactory方法中,可以擷取BeanDefinition的相關對象,并且修改該對象的屬性。
8.初始化Bean前後
有時,你想在初始化Bean前後,實作一些自己的邏輯。
這時可以實作:BeanPostProcessor接口。
該接口目前有兩個方法:
- postProcessBeforeInitialization 該在初始化方法之前調用。
- postProcessAfterInitialization 該方法再初始化方法之後調用。
例如:
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof User) {
((User) bean).setUserName("蘇三說技術");
}
return bean;
}
}
如果spring中存在User對象,則将它的userName設定成:蘇三說技術。
其實,我們經常使用的注解,比如:@Autowired、@Value、@Resource、@PostConstruct等,是通過AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor實作的。
9.初始化方法
目前spring中使用比較多的初始化bean的方法有:
- 使用@PostConstruct注解
- 實作InitializingBean接口
9.1 使用@PostConstruct注解
@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===初始化===");
}
}
在需要初始化的方法上增加@PostConstruct注解,這樣就有初始化的能力。
9.2 實作InitializingBean接口
@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===初始化===");
}
}
實作InitializingBean接口,重寫afterPropertiesSet方法,該方法中可以完成初始化功能。
10.關閉容器前
有時候,我們需要在關閉spring容器前,做一些額外的工作,比如:關閉資源檔案等。
這時可以實作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容器銷毀前,會調用該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接口:
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() {
}
}