掌握 Spring 框架這十個擴展點,讓你的能力更上一層樓
當(dāng)我們提到 Spring 時,或許首先映入腦海的是 IOC(控制反轉(zhuǎn))和 AOP(面向切面編程)。它們可以被視為 Spring 的基石。正是憑借其出色的設(shè)計,Spring 才能在眾多優(yōu)秀框架中脫穎而出。
Spring 具有很強的擴展性。許多第三方應(yīng)用程序,如 rocketmq、mybatis、redis 等,都可以輕松集成到 Spring 系統(tǒng)中。讓我們一起來看看 Spring 中最常用的十個擴展點。
1. 全局異常處理
過去,在開發(fā)接口時,如果發(fā)生異常,我們通常需要給用戶一個更友好的提示。但如果不進行錯誤處理,例如:
@RequestMapping("/test")
@RestController
public class TestController {
    @GetMapping("/division")
    public String division(@RequestParam("a") int a, @RequestParam("b")int b) {
        return String.valueOf(a / b);
    }
}這是一個計算 a/b 結(jié)果的方法,通過127.0.0.1:8080/test/division?a=10&b=2訪問后會出現(xiàn)以下結(jié)果:

什么?用戶能直接看到如此詳細的錯誤信息嗎?
這種報錯方式給用戶帶來了非常糟糕的體驗。為了解決這個問題,我們通常在接口中捕獲異常。
@GetMapping("/division")
public String division(@RequestParam("a") int a, @RequestParam("b") int b) {
    String result = "";
    try {
        result = String.valueOf(a / b);
    } catch (ArithmeticException e) {
        result = "params error";
    }
    return result;
}接口改造后,當(dāng)發(fā)生異常時,會提示:“params error”,用戶體驗會更好。
如果只是一個接口,那沒問題。但如果項目中有成百上千個接口,我們是否需要為所有接口添加異常處理代碼呢?
肯定不能這樣做的。這時,全局異常處理就派上用場了:RestControllerAdvice。
@RestControllerAdvice
publicclass GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public String handleException(Exception e) {
        if (e instanceof ArithmeticException) {
            return"params error";
        }
        if (e instanceof Exception) {
            return"Internal server exception";
        }
        returnnull;
    }
}只需在 handleException 方法中處理異常情況。業(yè)務(wù)接口可以放心使用,不再需要捕獲異常(遵循統(tǒng)一的處理邏輯)。
2. 自定義攔截器
與 Spring 攔截器相比,Spring MVC 攔截器可以在內(nèi)部獲取 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)) {
            returntrue;
        }
        returnfalse;
    }
    private boolean checkAuth(String requestUrl) {
        System.out.println("===Authority Verificatinotallow===");
        returntrue;
    }
}第二步,在 Spring 容器中注冊此攔截器。
@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {
    @Bean
    public AuthInterceptor getAuthInterceptor() {
        return new AuthInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
    }
}隨后,當(dāng)請求接口時,Spring MVC 可以通過此攔截器自動攔截接口并驗證權(quán)限。
3. 獲取 Spring 容器對象
在日常開發(fā)中,我們經(jīng)常需要從 Spring 容器中獲取 Beans。但是你知道如何獲取 Spring 容器對象嗎?
3.1 BeanFactoryAware 接口
@Service
public class StudentService implements BeanFactoryAware {
    private BeanFactory beanFactory;
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
    public void add() {
        Student student = (Student) beanFactory.getBean("student");
    }
}實現(xiàn) BeanFactoryAware 接口,然后重寫 setBeanFactory 方法。從這個方法中,可以獲取 Spring 容器對象。
3.2 ApplicationContextAware 接口
@Service
public class StudentService2 implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    public void add() {
        Student student = (Student) applicationContext.getBean("student");
    }
}4. 導(dǎo)入配置
有時我們需要在某個配置類中導(dǎo)入其他一些類,并且導(dǎo)入的類也會被添加到 Spring 容器中。此時,可以使用@Import 注解來完成此功能。
如果你看過它的源代碼,會發(fā)現(xiàn)導(dǎo)入的類支持三種不同的類型。
然而,我認為最好將普通類和帶有@Configuration 注解的配置類分開解釋。因此,列出了四種不同的類型:
4.1 導(dǎo)入普通類
這種導(dǎo)入方式最簡單。導(dǎo)入的類將被實例化為一個 bean 對象。
public class A {
}
@Import(A.class)
@Configuration
public class TestConfiguration {
}通過@Import 注解導(dǎo)入類 A,Spring 可以自動實例化對象 A。然后,可以在需要的地方通過@Autowired 注解進行注入:
@Autowired
private A a;是不是很神奇?不需要添加@Bean 注解就可以實例化對象。
4.2 導(dǎo)入帶有@Configuration 注解的配置類
這種導(dǎo)入方式最復(fù)雜,因為@Configuration 注解還支持多種組合注解,例如:
- @Import
 - @ImportResource
 - @PropertySource 等
 
public class A {
}
publicclass B {
}
@Import(B.class)
@Configuration
public class AConfiguration {
    @Bean
    public A a() {
        returnnew A();
    }
}
@Import(AConfiguration.class)
@Configuration
public class TestConfiguration {
}通過@Import 注解導(dǎo)入一個帶有@Configuration 注解的配置類,與該配置類相關(guān)的@Import、@ImportResource 和@PropertySource 等注解導(dǎo)入的所有類將一次性全部導(dǎo)入。
4.3 ImportSelector
這種導(dǎo)入方式需要實現(xiàn) ImportSelector 接口:
public class AImportSelector implements ImportSelector {
    private static final String CLASS_NAME = "com.demo.cache.service.A";
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{CLASS_NAME};
    }
}
@Import(AImportSelector.class)
@Configuration
public class TestConfiguration {
}這種方法的優(yōu)點是 selectImports 方法返回一個數(shù)組,這意味著可以非常方便的導(dǎo)入多個類。
4.4 ImportBeanDefinitionRegistrar
這種導(dǎo)入方式需要實現(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 {
}5. 項目啟動時的附加功能
有時我們需要在項目啟動時自定義一些附加邏輯,例如加載一些系統(tǒng)參數(shù)、資源初始化、預(yù)熱本地緩存等。我們該怎么做呢?Spring Boot 提供了兩個接口來幫助我們實現(xiàn)上述要求:
- CommandLineRunner
 - ApplicationRunner
 
它們的用法非常簡單。以 ApplicationRunner 接口為例:
@Component
publicclass MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 在這里編寫項目啟動時需要執(zhí)行的代碼
        System.out.println("項目啟動時執(zhí)行附加功能,加載系統(tǒng)參數(shù)...");
        // 假設(shè)這里從配置文件中加載系統(tǒng)參數(shù)并進行處理
        Properties properties = new Properties();
        try (InputStream inputStream = new FileInputStream("application.properties")) {
            properties.load(inputStream);
            String systemParam = properties.getProperty("system.param");
            System.out.println("加載的系統(tǒng)參數(shù)值為:" + systemParam);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}在上述代碼中,我們實現(xiàn)了 ApplicationRunner 接口,并重寫了 run 方法。在 run 方法中,我們可以編寫在項目啟動時需要執(zhí)行的附加功能代碼,例如加載系統(tǒng)參數(shù)、初始化資源、預(yù)熱緩存等。這里只是簡單地模擬了從配置文件中加載系統(tǒng)參數(shù)并打印出來,實際應(yīng)用中可以根據(jù)具體需求進行更復(fù)雜的操作。
當(dāng)項目啟動時,Spring Boot 會自動檢測并執(zhí)行實現(xiàn)了 ApplicationRunner 或 CommandLineRunner 接口的類中的 run 方法,從而實現(xiàn)項目啟動時的附加功能。
這兩個接口的區(qū)別在于參數(shù)類型不同,ApplicationRunner 的 run 方法參數(shù)是 ApplicationArguments,它提供了更多關(guān)于應(yīng)用程序參數(shù)的信息,而 CommandLineRunner 的 run 方法參數(shù)是原始的字符串?dāng)?shù)組,直接包含了命令行參數(shù)。根據(jù)具體需求可以選擇使用其中一個接口來實現(xiàn)項目啟動時的附加功能。
6. 修改 BeanDefinition
在實例化 Bean 對象之前,Spring IOC 需要先讀取 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", "Dylan Smith");
        defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
    }
}在 postProcessBeanFactory 方法中,可以獲取 BeanDefinition 的相關(guān)對象并修改該對象的屬性。
7. 初始化方法
目前,Spring 中比較常用的初始化 bean 的方法有:
- 使用@PostConstruct 注解。
 - 實現(xiàn) InitializingBean 接口。
 
7.1 使用@PostConstruct 注解
@Service
public class AService {
    @PostConstruct
    public void init() {
        System.out.println("===Initializing===");
    }
}在需要初始化的方法上添加@PostConstruct 注解。這樣,它就具有了初始化的能力。
7.2 實現(xiàn) InitializingBean 接口
@Service
public class BService implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("===Initializing===");
    }
}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("Dylan Smith");
        }
        return bean;
    }
}如果 Spring 中有一個 User 對象,將其 userName 設(shè)置為:Dylan Smith。
實際上,我們經(jīng)常使用的注解,如@Autowired、@Value、@Resource、@PostConstruct 等,都是通過 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 實現(xiàn)的。
9. 在關(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 接口,并覆蓋初始化方法和銷毀方法。
10. 自定義作用域
我們都知道,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 {
    privatestaticfinal 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();
        returnnull;
    }
    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
    }
    @Override
    public Object resolveContextualObject(String key) {
        returnnull;
    }
    @Override
    public String getConversationId() {
        returnnull;
    }
}第二步,將新定義的“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() {
    }
}總結(jié)
好了,今天的內(nèi)容就到這里。對 Spring 框架感興趣的讀者可以關(guān)注我,后續(xù)會分享更多有關(guān) Spring 的相關(guān)知識。















 
 
 









 
 
 
 