掌握 Spring 框架這十個擴展點,讓你的能力更上一層樓
當我們提到 Spring 時,或許首先映入腦海的是 IOC(控制反轉)和 AOP(面向切面編程)。它們可以被視為 Spring 的基石。正是憑借其出色的設計,Spring 才能在眾多優秀框架中脫穎而出。
Spring 具有很強的擴展性。許多第三方應用程序,如 rocketmq、mybatis、redis 等,都可以輕松集成到 Spring 系統中。讓我們一起來看看 Spring 中最常用的十個擴展點。
1. 全局異常處理
過去,在開發接口時,如果發生異常,我們通常需要給用戶一個更友好的提示。但如果不進行錯誤處理,例如:
@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 結果的方法,通過127.0.0.1:8080/test/division?a=10&b=2
訪問后會出現以下結果:
什么?用戶能直接看到如此詳細的錯誤信息嗎?
這種報錯方式給用戶帶來了非常糟糕的體驗。為了解決這個問題,我們通常在接口中捕獲異常。
@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;
}
接口改造后,當發生異常時,會提示:“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 方法中處理異常情況。業務接口可以放心使用,不再需要捕獲異常(遵循統一的處理邏輯)。
2. 自定義攔截器
與 Spring 攔截器相比,Spring MVC 攔截器可以在內部獲取 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)) {
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());
}
}
隨后,當請求接口時,Spring MVC 可以通過此攔截器自動攔截接口并驗證權限。
3. 獲取 Spring 容器對象
在日常開發中,我們經常需要從 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");
}
}
實現 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. 導入配置
有時我們需要在某個配置類中導入其他一些類,并且導入的類也會被添加到 Spring 容器中。此時,可以使用@Import 注解來完成此功能。
如果你看過它的源代碼,會發現導入的類支持三種不同的類型。
然而,我認為最好將普通類和帶有@Configuration 注解的配置類分開解釋。因此,列出了四種不同的類型:
4.1 導入普通類
這種導入方式最簡單。導入的類將被實例化為一個 bean 對象。
public class A {
}
@Import(A.class)
@Configuration
public class TestConfiguration {
}
通過@Import 注解導入類 A,Spring 可以自動實例化對象 A。然后,可以在需要的地方通過@Autowired 注解進行注入:
@Autowired
private A a;
是不是很神奇?不需要添加@Bean 注解就可以實例化對象。
4.2 導入帶有@Configuration 注解的配置類
這種導入方式最復雜,因為@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 注解導入一個帶有@Configuration 注解的配置類,與該配置類相關的@Import、@ImportResource 和@PropertySource 等注解導入的所有類將一次性全部導入。
4.3 ImportSelector
這種導入方式需要實現 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 {
}
這種方法的優點是 selectImports 方法返回一個數組,這意味著可以非常方便的導入多個類。
4.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 {
}
5. 項目啟動時的附加功能
有時我們需要在項目啟動時自定義一些附加邏輯,例如加載一些系統參數、資源初始化、預熱本地緩存等。我們該怎么做呢?Spring Boot 提供了兩個接口來幫助我們實現上述要求:
- CommandLineRunner
- ApplicationRunner
它們的用法非常簡單。以 ApplicationRunner 接口為例:
@Component
publicclass MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 在這里編寫項目啟動時需要執行的代碼
System.out.println("項目啟動時執行附加功能,加載系統參數...");
// 假設這里從配置文件中加載系統參數并進行處理
Properties properties = new Properties();
try (InputStream inputStream = new FileInputStream("application.properties")) {
properties.load(inputStream);
String systemParam = properties.getProperty("system.param");
System.out.println("加載的系統參數值為:" + systemParam);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述代碼中,我們實現了 ApplicationRunner 接口,并重寫了 run 方法。在 run 方法中,我們可以編寫在項目啟動時需要執行的附加功能代碼,例如加載系統參數、初始化資源、預熱緩存等。這里只是簡單地模擬了從配置文件中加載系統參數并打印出來,實際應用中可以根據具體需求進行更復雜的操作。
當項目啟動時,Spring Boot 會自動檢測并執行實現了 ApplicationRunner 或 CommandLineRunner 接口的類中的 run 方法,從而實現項目啟動時的附加功能。
這兩個接口的區別在于參數類型不同,ApplicationRunner 的 run 方法參數是 ApplicationArguments,它提供了更多關于應用程序參數的信息,而 CommandLineRunner 的 run 方法參數是原始的字符串數組,直接包含了命令行參數。根據具體需求可以選擇使用其中一個接口來實現項目啟動時的附加功能。
6. 修改 BeanDefinition
在實例化 Bean 對象之前,Spring IOC 需要先讀取 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", "Dylan Smith");
defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
}
}
在 postProcessBeanFactory 方法中,可以獲取 BeanDefinition 的相關對象并修改該對象的屬性。
7. 初始化方法
目前,Spring 中比較常用的初始化 bean 的方法有:
- 使用@PostConstruct 注解。
- 實現 InitializingBean 接口。
7.1 使用@PostConstruct 注解
@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===Initializing===");
}
}
在需要初始化的方法上添加@PostConstruct 注解。這樣,它就具有了初始化的能力。
7.2 實現 InitializingBean 接口
@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===Initializing===");
}
}
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("Dylan Smith");
}
return bean;
}
}
如果 Spring 中有一個 User 對象,將其 userName 設置為:Dylan Smith。
實際上,我們經常使用的注解,如@Autowired、@Value、@Resource、@PostConstruct 等,都是通過 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 實現的。
9. 在關閉容器之前添加操作
有時,我們需要在關閉 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 接口,并覆蓋初始化方法和銷毀方法。
10. 自定義作用域
我們都知道,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 {
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() {
}
}
總結
好了,今天的內容就到這里。對 Spring 框架感興趣的讀者可以關注我,后續會分享更多有關 Spring 的相關知識。