黑莲技术资源论坛

作者: 顺势而为47
查看: 108|回复: 0

聊聊Spring中最常用的11個擴展點

聊聊Spring中最常用的11個擴展點

[复制链接]
顺势而为47 | 显示全部楼层 发表于: 2022-9-22 20:44:24
顺势而为47 发表于: 2022-9-22 20:44:24 | 显示全部楼层 |阅读模式
查看: 108|回复: 0
前言

我們一說到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容器︰
@Configurationpublic 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接口

@Servicepublic 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接口

@Servicepublic 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接口

@Servicepublic 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")@RestControllerpublic 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。
@RestControllerAdvicepublic 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︰
@Datapublic 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容器中︰
@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter {    @Override    public void addFormatters(FormatterRegistry registry) {        registry.addConverter(new DateConverter());    }}復制代碼第四步,調用接口
@RequestMapping("/user")@RestControllerpublic 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)@Configurationpublic class TestConfiguration {}復制代碼通過@Import注解引入A類,spring就能自動實例化A對象,然後在需要使用的地方通過@Autowired注解注入即可︰
@Autowiredprivate A a;復制代碼是不是挺讓人意外的?不用加@Bean注解也能實例化bean。
5.2 配置類

這種引入方式是最復雜的,因為@Configuration注解還支持多種組合注解,比如︰

  • @Import
  • @ImportResource
  • @PropertySource等。
public class A {}public class B {}@Import(B.class)@Configurationpublic class AConfiguration {    @Bean    public A a() {        return new A();    }}@Import(AConfiguration.class)@Configurationpublic 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)@Configurationpublic 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)@Configurationpublic class TestConfiguration {}復制代碼這種方式是最靈活的,能在registerBeanDefinitions方法中獲取到BeanDefinitionRegistry容器注冊對象,可以手動控制BeanDefinition的創建和注冊。
6.項目啟動時

有時候我們需要在項目啟動時定制化一些附加功能,比如︰加載一些系統參數、完成初始化、預熱本地緩存等,該怎麼辦呢?
好消息是springboot提供了︰

  • CommandLineRunner
  • ApplicationRunner
這兩個接口幫助我們實現以上需求。
它們的用法還是挺簡單的,以ApplicationRunner接口為例︰
@Componentpublic 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接口。
@Componentpublic 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 該方法再初始化方法之後調用。
例如︰
@Componentpublic 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注解

@Servicepublic class AService {    @PostConstruct    public void init() {        System.out.println("===初始化===");    }}復制代碼在需要初始化的方法上增加@PostConstruct注解,這樣就有初始化的能力。
9.2 實現InitializingBean接口

@Servicepublic class BService implements InitializingBean {    @Override    public void afterPropertiesSet() throws Exception {        System.out.println("===初始化===");    }}復制代碼實現InitializingBean接口,重寫afterPropertiesSet方法,該方法中可以完成初始化功能。
10.關閉容器前

有時候,我們需要在關閉spring容器前,做一些額外的工作,比如︰關閉資源文件等。
這時可以實現DisposableBean接口,並且重寫它的destroy方法︰
@Servicepublic 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容器中︰
@Componentpublic class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {        beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());    }}復制代碼第三步使用新定義的Scope︰
@Scope("threadLocalScope")@Servicepublic class CService {    public void add() {    }}


作者︰甦三說技術
鏈接︰https://juejin.cn/post/7145084738775023646
來源︰稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|小黑屋|黑莲技术资源论坛 ( 闽ICP备18016623号-7 )|网站地图

GMT+8, 2022-10-7 21:14 , Processed in 0.439460 second(s), 26 queries .

Powered by BBS.HL1.NET X3.4 © 2020-2022

本站IT社区(bbs.hl1.net)所有的资源教程均来自网友分享及互联网收集

快速回复 返回顶部 返回列表