掌握 Spring 框架這十個(gè)擴(kuò)展點(diǎn),讓你的能力更上一層樓
當(dāng)我們提到 Spring 時(shí),或許首先映入腦海的是 IOC(控制反轉(zhuǎn))和 AOP(面向切面編程)。它們可以被視為 Spring 的基石。正是憑借其出色的設(shè)計(jì),Spring 才能在眾多優(yōu)秀框架中脫穎而出。
Spring 具有很強(qiáng)的擴(kuò)展性。許多第三方應(yīng)用程序,如 rocketmq、mybatis、redis 等,都可以輕松集成到 Spring 系統(tǒng)中。讓我們一起來(lái)看看 Spring 中最常用的十個(gè)擴(kuò)展點(diǎn)。
1. 全局異常處理
過(guò)去,在開(kāi)發(fā)接口時(shí),如果發(fā)生異常,我們通常需要給用戶一個(gè)更友好的提示。但如果不進(jìn)行錯(cuò)誤處理,例如:
@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping("/division")
public String division(@RequestParam("a") int a, @RequestParam("b")int b) {
return String.valueOf(a / b);
}
}
這是一個(gè)計(jì)算 a/b 結(jié)果的方法,通過(guò)127.0.0.1:8080/test/division?a=10&b=2
訪問(wèn)后會(huì)出現(xiàn)以下結(jié)果:
什么?用戶能直接看到如此詳細(xì)的錯(cuò)誤信息嗎?
這種報(bào)錯(cuò)方式給用戶帶來(lái)了非常糟糕的體驗(yàn)。為了解決這個(gè)問(wèn)題,我們通常在接口中捕獲異常。
@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ā)生異常時(shí),會(huì)提示:“params error”,用戶體驗(yàn)會(huì)更好。
如果只是一個(gè)接口,那沒(méi)問(wèn)題。但如果項(xiàng)目中有成百上千個(gè)接口,我們是否需要為所有接口添加異常處理代碼呢?
肯定不能這樣做的。這時(shí),全局異常處理就派上用場(chǎng)了: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 對(duì)象實(shí)例。
Spring MVC 攔截器的頂級(jí)接口是:HandlerInterceptor,它包含三個(gè)方法:
- preHandle:在目標(biāo)方法執(zhí)行前執(zhí)行。
- postHandle:在目標(biāo)方法執(zhí)行后執(zhí)行。
- afterCompletion:在請(qǐng)求完成時(shí)執(zhí)行。
為了方便起見(jiàn),在一般情況下,我們通常使用 HandlerInterceptor 接口的實(shí)現(xiàn)類(lèi) HandlerInterceptorAdapter。
如果存在權(quán)限認(rèn)證、日志記錄和統(tǒng)計(jì)等場(chǎng)景,可以使用此攔截器。
第一步,通過(guò)繼承 HandlerInterceptorAdapter 類(lèi)定義一個(gè)攔截器:
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 容器中注冊(cè)此攔截器。
@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)請(qǐng)求接口時(shí),Spring MVC 可以通過(guò)此攔截器自動(dòng)攔截接口并驗(yàn)證權(quán)限。
3. 獲取 Spring 容器對(duì)象
在日常開(kāi)發(fā)中,我們經(jīng)常需要從 Spring 容器中獲取 Beans。但是你知道如何獲取 Spring 容器對(duì)象嗎?
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");
}
}
實(shí)現(xiàn) BeanFactoryAware 接口,然后重寫(xiě) setBeanFactory 方法。從這個(gè)方法中,可以獲取 Spring 容器對(duì)象。
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)入配置
有時(shí)我們需要在某個(gè)配置類(lèi)中導(dǎo)入其他一些類(lèi),并且導(dǎo)入的類(lèi)也會(huì)被添加到 Spring 容器中。此時(shí),可以使用@Import 注解來(lái)完成此功能。
如果你看過(guò)它的源代碼,會(huì)發(fā)現(xiàn)導(dǎo)入的類(lèi)支持三種不同的類(lèi)型。
然而,我認(rèn)為最好將普通類(lèi)和帶有@Configuration 注解的配置類(lèi)分開(kāi)解釋。因此,列出了四種不同的類(lèi)型:
4.1 導(dǎo)入普通類(lèi)
這種導(dǎo)入方式最簡(jiǎn)單。導(dǎo)入的類(lèi)將被實(shí)例化為一個(gè) bean 對(duì)象。
public class A {
}
@Import(A.class)
@Configuration
public class TestConfiguration {
}
通過(guò)@Import 注解導(dǎo)入類(lèi) A,Spring 可以自動(dòng)實(shí)例化對(duì)象 A。然后,可以在需要的地方通過(guò)@Autowired 注解進(jìn)行注入:
@Autowired
private A a;
是不是很神奇?不需要添加@Bean 注解就可以實(shí)例化對(duì)象。
4.2 導(dǎo)入帶有@Configuration 注解的配置類(lèi)
這種導(dǎo)入方式最復(fù)雜,因?yàn)锧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 {
}
通過(guò)@Import 注解導(dǎo)入一個(gè)帶有@Configuration 注解的配置類(lèi),與該配置類(lèi)相關(guān)的@Import、@ImportResource 和@PropertySource 等注解導(dǎo)入的所有類(lèi)將一次性全部導(dǎo)入。
4.3 ImportSelector
這種導(dǎo)入方式需要實(shí)現(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)點(diǎn)是 selectImports 方法返回一個(gè)數(shù)組,這意味著可以非常方便的導(dǎo)入多個(gè)類(lèi)。
4.4 ImportBeanDefinitionRegistrar
這種導(dǎo)入方式需要實(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 {
}
5. 項(xiàng)目啟動(dòng)時(shí)的附加功能
有時(shí)我們需要在項(xiàng)目啟動(dòng)時(shí)自定義一些附加邏輯,例如加載一些系統(tǒng)參數(shù)、資源初始化、預(yù)熱本地緩存等。我們?cè)撛趺醋瞿??Spring Boot 提供了兩個(gè)接口來(lái)幫助我們實(shí)現(xiàn)上述要求:
- CommandLineRunner
- ApplicationRunner
它們的用法非常簡(jiǎn)單。以 ApplicationRunner 接口為例:
@Component
publicclass MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 在這里編寫(xiě)項(xiàng)目啟動(dòng)時(shí)需要執(zhí)行的代碼
System.out.println("項(xiàng)目啟動(dòng)時(shí)執(zhí)行附加功能,加載系統(tǒng)參數(shù)...");
// 假設(shè)這里從配置文件中加載系統(tǒng)參數(shù)并進(jìn)行處理
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();
}
}
}
在上述代碼中,我們實(shí)現(xiàn)了 ApplicationRunner 接口,并重寫(xiě)了 run 方法。在 run 方法中,我們可以編寫(xiě)在項(xiàng)目啟動(dòng)時(shí)需要執(zhí)行的附加功能代碼,例如加載系統(tǒng)參數(shù)、初始化資源、預(yù)熱緩存等。這里只是簡(jiǎn)單地模擬了從配置文件中加載系統(tǒng)參數(shù)并打印出來(lái),實(shí)際應(yīng)用中可以根據(jù)具體需求進(jìn)行更復(fù)雜的操作。
當(dāng)項(xiàng)目啟動(dòng)時(shí),Spring Boot 會(huì)自動(dòng)檢測(cè)并執(zhí)行實(shí)現(xiàn)了 ApplicationRunner 或 CommandLineRunner 接口的類(lèi)中的 run 方法,從而實(shí)現(xiàn)項(xiàng)目啟動(dòng)時(shí)的附加功能。
這兩個(gè)接口的區(qū)別在于參數(shù)類(lèi)型不同,ApplicationRunner 的 run 方法參數(shù)是 ApplicationArguments,它提供了更多關(guān)于應(yīng)用程序參數(shù)的信息,而 CommandLineRunner 的 run 方法參數(shù)是原始的字符串?dāng)?shù)組,直接包含了命令行參數(shù)。根據(jù)具體需求可以選擇使用其中一個(gè)接口來(lái)實(shí)現(xiàn)項(xiàng)目啟動(dòng)時(shí)的附加功能。
6. 修改 BeanDefinition
在實(shí)例化 Bean 對(duì)象之前,Spring IOC 需要先讀取 Bean 的相關(guān)屬性,將它們保存在 BeanDefinition 對(duì)象中,然后通過(guò) 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", "Dylan Smith");
defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
}
}
在 postProcessBeanFactory 方法中,可以獲取 BeanDefinition 的相關(guān)對(duì)象并修改該對(duì)象的屬性。
7. 初始化方法
目前,Spring 中比較常用的初始化 bean 的方法有:
- 使用@PostConstruct 注解。
- 實(shí)現(xiàn) InitializingBean 接口。
7.1 使用@PostConstruct 注解
@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===Initializing===");
}
}
在需要初始化的方法上添加@PostConstruct 注解。這樣,它就具有了初始化的能力。
7.2 實(shí)現(xiàn) InitializingBean 接口
@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===Initializing===");
}
}
8. 在初始化 Bean 前后添加邏輯
有時(shí),你希望在初始化 bean 之前和之后實(shí)現(xiàn)一些自己的邏輯。
這時(shí),可以實(shí)現(xiàn) BeanPostProcessor 接口。
這個(gè)接口目前有兩個(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("Dylan Smith");
}
return bean;
}
}
如果 Spring 中有一個(gè) User 對(duì)象,將其 userName 設(shè)置為:Dylan Smith。
實(shí)際上,我們經(jīng)常使用的注解,如@Autowired、@Value、@Resource、@PostConstruct 等,都是通過(guò) AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 實(shí)現(xiàn)的。
9. 在關(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 容器銷(xiāo)毀之前會(huì)調(diào)用 destroy 方法。通常,我們會(huì)同時(shí)實(shí)現(xiàn) InitializingBean 和 DisposableBean 接口,并覆蓋初始化方法和銷(xiāo)毀方法。
10. 自定義作用域
我們都知道,Spring 只支持兩種默認(rèn)的 Scope:
- singleton:在單例作用域中,從 Spring 容器中獲取的每個(gè) bean 都是同一個(gè)對(duì)象。
- prototype:在原型作用域中,從 Spring 容器中獲取的每個(gè) bean 都是不同的對(duì)象。
Spring Web 擴(kuò)展了 Scope 并添加了:
- RequestScope:在同一個(gè)請(qǐng)求中,從 Spring 容器中獲取的 bean 都是同一個(gè)對(duì)象。
- SessionScope:在同一個(gè)會(huì)話中,從 Spring 容器中獲取的 bean 都是同一個(gè)對(duì)象。
即便如此,有些場(chǎng)景仍然無(wú)法滿足我們的要求。
例如,如果我們希望在同一個(gè)線程中從 Spring 容器中獲取的所有 bean 都是同一個(gè)對(duì)象,該怎么辦呢?
這就需要自定義 Scope。
第一步,實(shí)現(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)容就到這里。對(duì) Spring 框架感興趣的讀者可以關(guān)注我,后續(xù)會(huì)分享更多有關(guān) Spring 的相關(guān)知識(shí)。