前言

咱们一说到spring,或许第一个想到的是 IOC(操控回转) 和 AOP(面向切面编程)。

没错,它们是spring的柱石,得益于它们的优异设计,使得spring能够从许多优异结构中脱颖而出。

除此之外,咱们在运用spring的过程中,有没有发现它的扩展才能十分强。由于这个优势的存在,让spring具有强大的容纳才能,让许多第三方应用能够轻松投入spring的怀有。比方:rocketmq、mybatis、redis等。

今天跟咱们一起聊聊,在Spring中最常用的11个扩展点。

聊聊 Spring 中最常用的 11 个扩展点

1.自界说阻拦器

spring mvc阻拦器根spring阻拦器相比,它里边能够获取HttpServletRequestHttpServletResponse等web方针实例。

spring mvc阻拦器的顶层接口是:HandlerInterceptor,包含三个办法:

  • preHandle 方针办法履行前履行
  • postHandle 方针办法履行后履行
  • afterCompletion 请求完结时履行

为了便利咱们一般状况会用HandlerInterceptor接口的完结类HandlerInterceptorAdapter类。

假设有权限认证、日志、统计的场景,能够运用该阻拦器。

第一步,承继HandlerInterceptorAdapter类界说阻拦器:

publicclassAuthInterceptorextendsHandlerInterceptorAdapter{
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)
throwsException{
StringrequestUrl=request.getRequestURI();
if(checkAuth(requestUrl)){
returntrue;
}
returnfalse;
}
privatebooleancheckAuth(StringrequestUrl){
System.out.println("===权限校验===");
returntrue;
}
}

第二步,将该阻拦器注册到spring容器:

@Configuration
publicclassWebAuthConfigextendsWebMvcConfigurerAdapter{

@Bean
publicAuthInterceptorgetAuthInterceptor(){
returnnewAuthInterceptor();
}
@Override
publicvoidaddInterceptors(InterceptorRegistryregistry){
registry.addInterceptor(newAuthInterceptor());
}
}

第三步,在请求接口时spring mvc经过该阻拦器,能够主动阻拦该接口,而且校验权限。

2.获取Spring容器方针

在咱们日常开发中,常常需求从Spring容器中获取Bean,但你知道如何获取Spring容器方针吗?

2.1 BeanFactoryAware接口

@Service
publicclassPersonServiceimplementsBeanFactoryAware{
privateBeanFactorybeanFactory;
@Override
publicvoidsetBeanFactory(BeanFactorybeanFactory)throwsBeansException{
this.beanFactory=beanFactory;
}
publicvoidadd(){
Personperson=(Person)beanFactory.getBean("person");
}
}

完结BeanFactoryAware接口,然后重写setBeanFactory办法,就能从该办法中获取到spring容器方针。

2.2 ApplicationContextAware接口

@Service
publicclassPersonService2implementsApplicationContextAware{
privateApplicationContextapplicationContext;
@Override
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
this.applicationContext=applicationContext;
}
publicvoidadd(){
Personperson=(Person)applicationContext.getBean("person");
}
}

完结ApplicationContextAware接口,然后重写setApplicationContext办法,也能从该办法中获取到spring容器方针。

2.3 ApplicationListener接口

@Service
publicclassPersonService3implementsApplicationListener<ContextRefreshedEvent>{
privateApplicationContextapplicationContext;
@Override
publicvoidonApplicationEvent(ContextRefreshedEventevent){
applicationContext=event.getApplicationContext();
}
publicvoidadd(){
Personperson=(Person)applicationContext.getBean("person");
}
}

3.大局反常处理

以前咱们在开发接口时,假设出现反常,为了给用户一个更友爱的提示,例如:

@RequestMapping("/test")
@RestController
publicclassTestController{
@GetMapping("/add")
publicStringadd(){
inta=10/0;
return"成功";
}
}

假设不做任何处理请求add接口结果直接报错:

聊聊 Spring 中最常用的 11 个扩展点

what?用户能直接看到错误信息?

这种交互办法给用户的体会十分差,为了处理这个问题,咱们通常会在接口中捕获反常:

@GetMapping("/add")
publicStringadd(){
Stringresult="成功";
try{
inta=10/0;
}catch(Exceptione){
result="数据反常";
}
returnresult;
}

接口改造后,出现反常时会提示:“数据反常”,对用户来说更友爱。

看起来挺不错的,可是有问题。。。

假设仅仅一个接口还好,可是假设项目中有成百上千个接口,都要加上反常捕获代码吗?

答案是否定的,这时大局反常处理就派上用场了:RestControllerAdvice

@RestControllerAdvice
publicclassGlobalExceptionHandler{
@ExceptionHandler(Exception.class)
publicStringhandleException(Exceptione){
if(einstanceofArithmeticException){
return"数据反常";
}
if(einstanceofException){
return"服务器内部反常";
}
returnnull;
}
}

只需在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:

@Data
publicclassUser{
privateLongid;
privateStringname;
privateDateregisterDate;
}

第二步,完结Converter接口:

publicclassDateConverterimplementsConverter<String,Date>{
privateSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");
@Override
publicDateconvert(Stringsource){
if(source!=null&&!"".equals(source)){
try{
simpleDateFormat.parse(source);
}catch(ParseExceptione){
e.printStackTrace();
}
}
returnnull;
}
}

第三步,将新界说的类型转化器注入到spring容器中:

@Configuration
publicclassWebConfigextendsWebMvcConfigurerAdapter{
@Override
publicvoidaddFormatters(FormatterRegistryregistry){
registry.addConverter(newDateConverter());
}
}

第四步,调用接口

@RequestMapping("/user")
@RestController
publicclassUserController{
@RequestMapping("/save")
publicStringsave(@RequestBodyUseruser){
return"success";
}
}

请求接口时User方针中registerDate字段会被主动转化成Date类型。

5.导入装备

有时咱们需求在某个装备类中引进别的一些类,被引进的类也加到spring容器中。这时能够运用@Import注解完结这个功用。

假设你看过它的源码会发现,引进的类支撑三种不同类型。

可是我以为最好将一般类和@Configuration注解的装备类分隔讲解,所以列了四种不同类型:

聊聊 Spring 中最常用的 11 个扩展点

5.1 一般类

这种引进办法是最简略的,被引进的类会被实例化bean方针。

publicclassA{
}
@Import(A.class)
@Configuration
publicclassTestConfiguration{
}

经过@Import注解引进A类,spring就能主动实例化A方针,然后在需求运用的当地经过@Autowired注解注入即可:

@Autowired
privateAa;

是不是挺让人意外的?不用加@Bean注解也能实例化bean。

5.2 装备类

这种引进办法是最杂乱的,因为@Configuration注解还支撑多种组合注解,比方:

  • @Import
  • @ImportResource
  • @PropertySource等。
publicclassA{
}
publicclassB{
}
@Import(B.class)
@Configuration
publicclassAConfiguration{
@Bean
publicAa(){
returnnewA();
}
}
@Import(AConfiguration.class)
@Configuration
publicclassTestConfiguration{
}

经过@Import注解引进@Configuration注解的装备类,会把该装备类相关@Import@ImportResource@PropertySource等注解引进的类进行递归,一次性悉数引进。

5.3 ImportSelector

这种引进办法需求完结ImportSelector接口:

publicclassAImportSelectorimplementsImportSelector{
privatestaticfinalStringCLASS_NAME="com.sue.cache.service.test13.A";

publicString[]selectImports(AnnotationMetadataimportingClassMetadata){
returnnewString[]{CLASS_NAME};
}
}
@Import(AImportSelector.class)
@Configuration
publicclassTestConfiguration{
}

这种办法的优点是selectImports办法回来的是数组,意味着能够一起引进多个类,还是十分便利的。

5.4 ImportBeanDefinitionRegistrar

这种引进办法需求完结ImportBeanDefinitionRegistrar接口:

publicclassAImportBeanDefinitionRegistrarimplementsImportBeanDefinitionRegistrar{
@Override
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
RootBeanDefinitionrootBeanDefinition=newRootBeanDefinition(A.class);
registry.registerBeanDefinition("a",rootBeanDefinition);
}
}
@Import(AImportBeanDefinitionRegistrar.class)
@Configuration
publicclassTestConfiguration{
}

这种办法是最灵活的,能在registerBeanDefinitions办法中获取到BeanDefinitionRegistry容器注册方针,能够手动操控BeanDefinition的创建和注册。

6.项目启动时

有时候咱们需求在项目启动时定制化一些附加功用,比方:加载一些系统参数、完结初始化、预热本地缓存等,该怎么办呢?

好消息是springboot供给了:

  • CommandLineRunner
  • ApplicationRunner

这两个接口协助咱们完结以上需求。

它们的用法还是挺简略的,以ApplicationRunner接口为例:

@Component
publicclassTestRunnerimplementsApplicationRunner{
@Autowired
privateLoadDataServiceloadDataService;
publicvoidrun(ApplicationArgumentsargs)throwsException{
loadDataService.load();
}
}

完结ApplicationRunner接口,重写run办法,在该办法中完结自己定制化需求。

假设项目中有多个类完结了ApplicationRunner接口,他们的履行次序要怎么指定呢?

答案是运用@Order(n)注解,n的值越小越先履行。当然也能够经过@Priority注解指定次序。

7.修改BeanDefinition

Spring IOC在实例化Bean方针之前,需求先读取Bean的相关特点,保存到BeanDefinition方针中,然后经过BeanDefinition方针,实例化Bean方针。

假设想修改BeanDefinition方针中的特点,该怎么办呢?

答:咱们能够完结BeanFactoryPostProcessor接口。

@Component
publicclassMyBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{

@Override
publicvoidpostProcessBeanFactory(ConfigurableListableBeanFactoryconfigurableListableBeanFactory)throwsBeansException{
DefaultListableBeanFactorydefaultListableBeanFactory=(DefaultListableBeanFactory)configurableListableBeanFactory;
BeanDefinitionBuilderbeanDefinitionBuilder=BeanDefinitionBuilder.genericBeanDefinition(User.class);
beanDefinitionBuilder.addPropertyValue("id",123);
beanDefinitionBuilder.addPropertyValue("name","苏三说技能");
defaultListableBeanFactory.registerBeanDefinition("user",beanDefinitionBuilder.getBeanDefinition());
}
}

在postProcessBeanFactory办法中,能够获取BeanDefinition的相关方针,而且修改该方针的特点。

8.初始化Bean前后

有时,你想在初始化Bean前后,完结一些自己的逻辑。

这时能够完结:BeanPostProcessor接口。

该接口现在有两个办法:

  • postProcessBeforeInitialization 该在初始化办法之前调用。
  • postProcessAfterInitialization 该办法再初始化办法之后调用。

例如:

@Component
publicclassMyBeanPostProcessorimplementsBeanPostProcessor{
@Override
publicObjectpostProcessAfterInitialization(Objectbean,StringbeanName)throwsBeansException{
if(beaninstanceofUser){
((User)bean).setUserName("苏三说技能");
}
returnbean;
}
}

假设spring中存在User方针,则将它的userName设置成:苏三说技能。

其实,咱们常常运用的注解,比方:@Autowired、@Value、@Resource、@PostConstruct等,是经过AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor完结的。

9.初始化办法

现在spring中运用比较多的初始化bean的办法有:

  1. 运用@PostConstruct注解
  2. 完结InitializingBean接口

9.1 运用@PostConstruct注解

@Service
publicclassAService{
@PostConstruct
publicvoidinit(){
System.out.println("===初始化===");
}
}

在需求初始化的办法上增加@PostConstruct注解,这样就有初始化的才能。

9.2 完结InitializingBean接口

@Service
publicclassBServiceimplementsInitializingBean{
@Override
publicvoidafterPropertiesSet()throwsException{
System.out.println("===初始化===");
}
}

完结InitializingBean接口,重写afterPropertiesSet办法,该办法中能够完结初始化功用。

10.封闭容器前

有时候,咱们需求在封闭spring容器前,做一些额定的工作,比方:封闭资源文件等。

这时能够完结DisposableBean接口,而且重写它的destroy办法:

@Service
publicclassDServiceimplementsInitializingBean,DisposableBean{

@Override
publicvoiddestroy()throwsException{
System.out.println("DisposableBeandestroy");
}

@Override
publicvoidafterPropertiesSet()throwsException{
System.out.println("InitializingBeanafterPropertiesSet");
}
}

这样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接口:

publicclassThreadLocalScopeimplementsScope{
privatestaticfinalThreadLocalTHREAD_LOCAL_SCOPE=newThreadLocal();
@Override
publicObjectget(Stringname,ObjectFactory<?>objectFactory){
Objectvalue=THREAD_LOCAL_SCOPE.get();
if(value!=null){
returnvalue;
}
Objectobject=objectFactory.getObject();
THREAD_LOCAL_SCOPE.set(object);
returnobject;
}
@Override
publicObjectremove(Stringname){
THREAD_LOCAL_SCOPE.remove();
returnnull;
}
@Override
publicvoidregisterDestructionCallback(Stringname,Runnablecallback){
}
@Override
publicObjectresolveContextualObject(Stringkey){
returnnull;
}
@Override
publicStringgetConversationId(){
returnnull;
}
}

第二步将新界说的Scope注入到spring容器中:

@Component
publicclassThreadLocalBeanFactoryPostProcessorimplementsBeanFactoryPostProcessor{
@Override
publicvoidpostProcessBeanFactory(ConfigurableListableBeanFactorybeanFactory)throwsBeansException{
beanFactory.registerScope("threadLocalScope",newThreadLocalScope());
}
}

第三步运用新界说的Scope:

@Scope("threadLocalScope")
@Service
publicclassCService{
publicvoidadd(){
}
}