我們一說到spring,可能第一個想到的是 IOC
(控制反轉) 和 AOP
(面向切面程式設計)。
沒錯,它們是spring的基石,得益於它們的優秀設計,使得spring能夠從眾多優秀框架中脫穎而出。
除此之外,我們在使用spring的過程中,有沒有發現它的擴充套件能力非常強
。由於這個優勢的存在,讓spring擁有強大的包容能力,讓很多第三方應用能夠輕鬆投入spring的懷抱。比如:rocketmq、mybatis、redis等。
今天跟大家一起聊聊,在Spring中最常用的11個擴充套件點。
spring mvc攔截器根spring攔截器相比,它裡面能夠獲取HttpServletRequest
和HttpServletResponse
等web物件範例。
spring mvc攔截器的頂層介面是:HandlerInterceptor
,包含三個方法:
為了方便我們一般情況會用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通過該攔截器,能夠自動攔截該介面,並且校驗許可權。
在我們日常開發中,經常需要從Spring容器中獲取Bean,但你知道如何獲取Spring容器物件嗎?
@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容器物件。
@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容器物件。
@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");
}
}
以前我們在開發介面時,如果出現異常,為了給使用者一個更友好的提示,例如:
@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
方法中處理異常情況,業務介面中可以放心使用,不再需要捕獲異常(有人統一處理了)。真是爽歪歪。
spring目前支援3中型別轉換器:
這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型別。
有時我們需要在某個設定類中引入另外一些類,被引入的類也加到spring容器中。這時可以使用@Import
註解完成這個功能。
如果你看過它的原始碼會發現,引入的類支援三種不同型別。
但是我認為最好將普通類和@Configuration註解的設定類分開講解,所以列了四種不同型別:
這種引入方式是最簡單的,被引入的類會被範例化bean物件。
public class A {
}
@Import(A.class)
@Configuration
public class TestConfiguration {
}
通過@Import
註解引入A類,spring就能自動範例化A物件,然後在需要使用的地方通過@Autowired
註解注入即可:
@Autowired
private A a;
是不是挺讓人意外的?不用加@Bean
註解也能範例化bean。
這種引入方式是最複雜的,因為@Configuration
註解還支援多種組合註解,比如:
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
等註解引入的類進行遞迴,一次性全部引入。
這種引入方式需要實現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
方法返回的是陣列,意味著可以同時引入多個類,還是非常方便的。
這種引入方式需要實現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的建立和註冊。
有時候我們需要在專案啟動時客製化化一些附加功能,比如:載入一些系統引數、完成初始化、預熱本地快取等,該怎麼辦呢?
好訊息是springboot提供了:
這兩個介面幫助我們實現以上需求。
它們的用法還是挺簡單的,以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
註解指定順序。
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的相關物件,並且修改該物件的屬性。
有時,你想在初始化Bean前後,實現一些自己的邏輯。
這時可以實現:BeanPostProcessor
介面。
該介面目前有兩個方法:
例如:
@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實現的。
目前spring中使用比較多的初始化bean的方法有:
@Service
public class AService {
@PostConstruct
public void init() {
System.out.println("===初始化===");
}
}
在需要初始化的方法上增加@PostConstruct
註解,這樣就有初始化的能力。
@Service
public class BService implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===初始化===");
}
}
實現InitializingBean
介面,重寫afterPropertiesSet
方法,該方法中可以完成初始化功能。
有時候,我們需要在關閉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介面,重寫初始化方法和銷燬方法。
我們都知道spring預設支援的Scope
只有兩種:
spring web又對Scope進行了擴充套件,增加了:
即便如此,有些場景還是無法滿足我們的要求。
比如,我們想在同一個執行緒中從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() {
}
}
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維條碼關注一下,您的支援是我堅持寫作最大的動力。
求一鍵三連:點贊、轉發、在看。
關注公眾號:【蘇三說技術】,在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。