Java后端学习路线β阶段--Spring的扩展功能开发及实战

100 阅读16分钟

大榜:学习和使用Spring框架有2年了,算是有了一点点经验,今天咱们一起讨论下Spring的扩展功能点。

小汪:好啊,一起讨论,共同进步!这次咱们讨论Spring的哪些功能点?

大榜:这次我们先讨论Spring中最重要的功能,也就是bean生命周期的扩展功能和自定义starter场景启动器,其他的扩展功能如Spring MVC,不在本次讨论范围内了。

小汪:Spring创建bean对象的完整生命周期流程是啥样的?你看,如果我定义了一个UserService类,通过Spring加入到IOC容器中,那Spring创建这个bean对象的流程是什么样的呢?

1、Spring bean生命周期的扩展功能

大榜:创建UserService这个bean对象,其完整的生命周期如下:

UserService类---》推断构造函数--》实例化对象---》填充属性(依赖注入,如@Autowired)---》Aware回调---》初始化前(如@PostConstruct)---》初始化---》初始化后---》将userService对象放入Map单例池---》使用---》当容器关闭时,销毁bean对象。

你看,首先Spring根据你定义的UserService类中的多个构造函数,推断选择某个构造函数,实例化得到UserService对象;接着对UserService对象中的属性进行赋值;然后处理程序员实现了Aawre接口回调;之后,进入初始化流程,初始化流程分为初始化前、初始化、初始化后这三个步骤,每个步骤,程序员就可以对UserService对象进行操作,相当于Spring给程序员们埋入了引线,程序员们可以最大限度地对bean对象进行操作。

当初始化后地步骤执行完成后,这个UserService的bean对象就创建成功了,Spring将这个bean对象放入Map单例池,键为bean的名字,值为bean对象。程序员下次使用时,就可以直接从Map单例池中查找并使用了。

小汪:我记得在Spring中,bean的实例化和初始化是有很大区别的。

大榜:实例化表示还没有给对象的属性赋值,是一个半成品。初始化:已经给bean对象的属性做了赋值,是一个成品对象,程序员可以直接使用。Spring创建完成后的bean对象,都是成品对象,程序员可以直接使用,不用我们操心。

1.1、Aware接口的扩展功能

小汪:Aware接口回调,感觉没用啊,平时开发中能用上吗,Aware接口回调的需求场景是什么样的呢?

大榜:一般来说,我们基于SpringBoot开发单体项目,可以通过@Autowired注解的方式,将ApplicationContext装配然后所在的类可以使用该ApplicationContext对象,代码如下:

@Component
class TaskService {
    @Autowired
    private ApplicationContext applicationContext;
    
    public void test() {
        System.out.println("打印," + applicationContext);
    }
    
}

上面的代码,首先使用@Component注解将UserService注入到IOC容器,接着在UserService类中,定义了private ApplicationContext applicationContext; 并使用@Autowired将applicationContext装配,这样test方法中就可以使用applicationContext了。

如果执行test方法,会打印applicationContext对象的内存地址。

如果我们定义了一个Task类,如下:

// 注意:此处没有@Component注解
class Task {
    
    // @Autowired 如果加上@Autowired,会报错
    private ApplicationContext applicationContext;
    
    public void doTask() {
        System.out.println("打印," + applicationContext);
    }
    
}

实际输出结果:打印,null

根据输出结果表明,Task类中的属性applicationContext并没有加入到IOC容器中。但是,如果有一个需求场景,Task类也想要使用Spring提供的ApplicationContext对象,但目前的情况是获取的applicationContext为null。那该怎么办呢?这个时候,Aware接口就派上用场了,通过Aware接口,我们可以在Spring启动期间获取得到ApplicationContext对象,便于后续使用。代码是下面这样的:

package com.cloud.common.util;
​
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
​
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
​
    private static ApplicationContext context;
​
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
​
    public static <T> T getBean(Class<T> clazz){
        return context.getBean(clazz);
    }
​
    public static <T> T getBeanByName(String beanName){
        return (T)context.getBean(beanName);
    }
​
}

上面的代码中,我们定义了ApplicationContextUtils类,实现了Spring的ApplicationContextAware接口,并将ApplicationContextUtils对象注入到IOC容器中,其作用是在系统启动时获取ApplicationContext对象。然后提供了getBean、getBeanByName方法,用于获取容器中注入的bean对象。下面是根据ApplicationContextUtils来获取容器中的redisOperator对象,代码如下:

class Task {
    private RedisOperator redisOperator = ApplicationContextUtils.getBean(RedisOperator.class);
    
    public void doTask() {
        
        System.out.println("打印redisOperator," + redisOperator);
    }
    
}

输出结果:打印redisOperator,com.bang.RedisOperator@27ee0dc9

你看,当项目启动时,调用Task类的doTask方法后,就会打印上面的内容。

小汪:除了ApplicationContextAware,应该还有其他Aware接口把?

大榜:还有与环境相关的接口EnvironmentAware、与bean名称相关的BeanNameAware。EnvironmentAware用于在Spring启动期间获取系统环境信息,BeanNameAware用于Spring启动期间重新设置bean对象的名字。

1.2、bean初始化及销毁过程的扩展功能

小汪:Aware接口,我搞懂了。那初始化流程,Spring给我们程序员埋下了引线,那我们该如何操作bean对象呢?

1.2.1、初始化前

大榜:初始化流程分为 初始化前、初始化、初始化后这3个步骤,当容器关闭时,需要释放销毁这些bean对象。我们一个一个往下讨论,首先是初始化前的扩展功能。

小汪:初始化前,也就是bean对象初始化之前,那我们程序员能对这个bean对象干什么呢?

榜哥:我们可以打印指定的bean的名字,也可以给属性做初始化赋值操作。

小汪:那Spring提供了哪些引线(也就是接口或工具),让我们来操作初始化之前的逻辑呢?

大榜:Spring为我们提供了2根引线,我们可以通过这2根引线来干预初始化之前的逻辑。这2根引线分别是:

@PostConstruct注解、BeanPostProcessor接口。

第一根引线是@PostConstruct注解,其用法如下:

    // 构造函数执行(对象实例化)之后,会执行该注解下面的方法。初始化之前被调用
    @PostConstruct
    public void init(){
        System.out.println("Dog....@PostConstruct...");
    }

你看,我们在init方法上标注了该注解,那么Spring启动时,就知道在bean对象初始化之前,去调用init方法。

小汪:不对把,init()所在的类必须加入到IOC容器,才会生效。要不然,Spring都没有管理这个类,肯定也不会调用init方法的。如果我们想知道bean对象,是否加入到IOC容器,可以将容器中的beanName打印出来,这样就可以根据beanName来判断bean对象是否已加入到Spring容器,代码如下:

/**
*  输出容器中所有的组件名称
*  @param applicationContext
*/
private void printBeans(ApplicationContext applicationContext){
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
}

大榜:你说得对,是需要将init方法所在的类加入到容器,是我忘了。接下来,我们说说第二根引线:BeanPostProcessor接口。接口是用来实现的,显然我们程序员需要实现该接口,完整的代码如下:

package com.atguigu.bean.dog;
​
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
​
/**
 * 后置处理器:在bean初始化前后进行处理工作
 * 使用@Component注解,将后置处理器加入到容器中
 *
 */
//@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
​
    // 在bean对象初始化之前被调用
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if ("dog".equals(beanName)) {
            System.out.println("初始化前 postProcessBeforeInitialization..."+beanName+" => "+bean);
        }
        return bean;
    }
​
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if ("dog".equals(beanName)) {
            System.out.println("初始化后 postProcessAfterInitialization..."+beanName+" => "+bean);
        }
        return bean;
    }
​
}

上面的代码中,我们定义了一个类MyBeanPostProcessor,实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization、postProcessAfterInitialization这两个方法。其中,postProcessBeforeInitialization方法是初始化之前的执行逻辑,这个方法中首先判断有没有叫"dog"的beanName,如果有,则打印出来。

小汪:哇哇,Spring的这2根引线埋的好妙啊,我们程序员只需要使用注解@PostConstruct或者实现BeanPostProcessor接口,就可以干预bean对象的初始化了,扩展性好强大啊。

1.2.2、初始化

大榜:那接下来我们看看第2个步骤:初始化。看看Spring在初始化时,又给我们埋下了什么引线?

小汪:这个我知道。好像只有一根引线,就是InitializingBean接口。

大榜:是滴了。我们可以实现InitializingBean接口,来干预初始化过程,代码如下:

package com.atguigu.bean.dog;
​
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
​
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
​
/**
 * 实现Aware接口
 */
@Component
public class Dog implements  InitializingBean {
    
    public Dog(){
        System.out.println("Dog...实例化,调用构造函数");
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Dog...初始化 afterPropertiesSet...");
    }
}

1.2.3、初始化后

小汪:Spring在初始化后,又埋下了什么引线呢?

大榜:初始化后的引线与初始化前的引线差不多,都有BeanPostProcessor接口。我们可以实现BeanPostProcessor接口,重写了postProcessAfterInitialization方法,来干预初始化后的逻辑,动态代理中代理对象的创建是在初始化后完成的。初始化后的代码如下:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    if ("dog".equals(beanName)) {
        System.out.println("初始化后 postProcessAfterInitialization..."+beanName+" => "+bean);
    }
    return bean;
}

1.2.4、销毁

小汪:那接下我们看看当容器关闭时,销毁bean对象,Spring为我们提供的引线是什么?

大榜:销毁阶段的引线是注解@PreDestroy。我们只需要在方法上标注该注解,同时方法所在的类需要被加入到Spring容器中,这与@PostContruct注解的方法一样。销毁阶段的干预逻辑,如下:

// 容器移除对象之前,会调用@PreDestroy方法
    @PreDestroy
    public void detory(){
        System.out.println("Dog....@PreDestroy...");
    }

1.3、Spring bean对象生命周期的总结

小汪:我们前面讨论了,创建UserService这个bean对象的完整的生命周期,流程这么长,所以说bean对象的创建是很复杂的,我们一起总结下。

大榜:是需要总结下,免得以后忘记了。那我们一起来总结和归纳下,主要包括Aware回调、初始化过程、销毁的完整代码,如下所示:

package com.atguigu.bean.dog;
​
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
​
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
​
/**
 * 实现Aware接口、InitializingBean接口
 */
@Component
public class Dog implements ApplicationContextAware, InitializingBean {
    
    @Autowired
    private ApplicationContext applicationContext;
    
    public Dog(){
        System.out.println("Dog...实例化,调用构造函数");
    }
    
    @Override // applicationContext,也就是IOC容器。
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("Dog...ApplicationContextAware接口回调");
        this.applicationContext = applicationContext;
    }
​
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Dog...初始化 afterPropertiesSet...");
    }
​
    // 构造函数执行(对象实例化)之后,会执行该注解下面的方法
    @PostConstruct
    public void init(){
        System.out.println("Dog....初始化前 @PostConstruct...");
    }
​
    // 容器移除对象之前,会调用@PreDestroy方法
    @PreDestroy
    public void detory(){
        System.out.println("Dog....销毁 @PreDestroy...");
    }
​
}

输出结果:

Dog...实例化,调用构造函数
Dog...ApplicationContextAware接口回调
Dog....初始化前 @PostConstruct...
Dog...初始化 afterPropertiesSet...
MainConfigOfLifeCycle配置类,IOC容器创建完成...
七月 13, 2022 3:02:49 下午 org.springframework.context.annotation.AnnotationConfigApplicationContext doClose
信息: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3cd1f1c8: startup date [Wed Jul 13 15:02:49 CST 2022]; root of context hierarchy
Dog....销毁 @PreDestroy...
​
Process finished with exit code 0

上面的代码中,我们实现了Aware接口、初始化过程、销毁,基本上包含了创建bean对象的完整生命周期,以后可以直接拿去使用。

2、自定义starter场景启动器

小汪:你知道Spring Boot的自动配置原理吗,面试官要我自定义一个starter场景启动器,我当时听了一脸懵逼。

大榜:哈哈哈,面试官这波骚操作。不过,编写starter,可以让我们更加熟悉Spring Boot的底层原理。话不多说,开干吧!自动装配的原理其实很简单,就是Spring Boot 通过@EnableAutoConfiguration开启自动装配,通过 SpringFactoriesLoader 最终加载META-INF/spring.factories中的自动配置类来实现自动装配,自动配置类其实就是通过@Conditional按需加载的配置类。

举个栗子吧,我们开发Web后端应用时,如果不是Spring Boot项目,我们程序员要想使用@Controller、@RequestMapping来开发Web应用,则需要手动注入Spring MVC相关的bean组件,如DispatherServlet等,这种方式比较麻烦,而且容易忘记。

如果是Spring Boot项目,只需要在pom.xml中引入spring-boot-starter-web依赖包,就可以使用@Controller、@RequestMapping来开发Web应用了,我们自己不需要注入相关bean组件。

小汪:为什么在Spring Boot中,引入spring-boot-starter-web依赖包后,就不需要手动注入DispatherServlet等bean组件了呢,这是为什么?

大榜:这与我们刚刚讲的自动配置原理有关。说白了,spring-boot-starter-web依赖包中,有一个自动配置类,这个自动配置类在系统启动时加载,并将DispatherServlet组件自动加入到IOC容器中。经过这样后,我们程序员就不用手动注入该组件了,因为Spring Boot帮我们做好了。

2.1、如何实现一个starter

小汪:榜哥, 你刚刚说的是Spring Boot实现的starter自动配置,如果我自己想实现一个starter,该如何实现呢?

大榜:咱们照葫芦画瓢,按照Spring Boot的实现,我们自己来实现一下。步骤是下面这样的:

首先定义一个自动配置类;接着spring.factories文件中声明自动配置类;最后编写测试方法。具体如下:

1)定义一个自动配置类

package com.bang;
​
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
​
/**
 * @author qinxubang
 * @Date 2022/7/13 15:19
 */
@Configuration
public class ThreadPoolAutoConfiguration {
​
    @Bean
    @ConditionalOnClass(ThreadPoolExecutor.class)
    public ThreadPoolExecutor myThreadPool() {
        return new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10000));
    }
​
}

上面的代码中,我们定义了一个自动配置类,然后通过条件注解,按照程序中是否有ThreadPoolExecutor.class类这个条件,如果该条件成立,执行myThreadPool方法将ThreadPoolExecutor对象加入到IOC容器;如果条件不成立,则不执行myThreadPool方法。

2)spring.factories文件中声明自动配置类

在项目的META-INF/spring.factories文件中,声明自动配置类ThreadPoolAutoConfiguration,如下所示:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.bang.ThreadPoolAutoConfiguration

3)编写测试方法

最后,我们编写测试方法,来验证ThreadPoolExecutor对象,是否加入到IOC容器。测试代码如下所示:

package com.atguigu.boot;
​
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.concurrent.ThreadPoolExecutor;
​
@SpringBootTest
class Boot09HelloTestApplicationTests {
​
    @Autowired
    private ThreadPoolExecutor threadPool;
​
    @Test
    void contextLoads() {
        System.out.println("启动类中配置的最大核心线程数:" + threadPool.getCorePoolSize());
    }
​
}
​

输出结果:

启动类中配置的最大核心线程数:10

根据输出结果,我们可以得出ThreadPoolExecutor对象已经加入到IOC容器了。

小汪:听你这么一说,我感觉自定义starter场景启动器,很简单啊。

大榜:很简单的原因是因为我们自定义starter很简单,因为ThreadPoolAutoConfiguration 自动配置类中,只按条件注入了ThreadPoolExecutor对象,复杂的自动配置类中可能会注入多个bean对象,并按照各种条件进行加载使用。下面的全局异常处理器的starter、分布式锁的starter就是比较复杂的场景启动器。

小汪:全局异常处理器的starter,我猜应该是对异常进行处理,它是如何实现的呢?

2.2、全局异常处理器的starter

大榜:全局异常处理器分为Controller层的异常处理器、非Controller层的异常处理器。我们分开来讨论,讲解起来不容器混,思路也更清晰。

2.2.1、Controller层的异常处理器

小汪:对于接口的异常处理,我直接使用try、catch语句来处理异常也行啊。为什么需要使用Controller层的异常处理器呀?

大榜:如果后端接口比较少,要是产生异常了,我们可以对每个接口使用try、catch语句来处理异常。

但如果有100个接口呢,你是不是需要自己来写100个异常处理了呢?Controller层的异常处理器可以由Spring MVC框架提供,程序员可以使用@RestControllerAdvice、 @ExceptionHandler注解来定义异常捕获逻辑,可以参考拙作:Controller层的全局异常处理器

2.2.2、非Controller层的异常处理器

小汪:那非Controller层的异常处理器,也是Spring来实现的吗?

大榜:这个是我参考Spring源码实现的。Spring使用@RestControllerAdvice来标注在类上面,作用于异常处理类;使用@ExceptionHandler注解标注在方法上,捕获具体的异常类型方法,并编写捕获之后的代码逻辑。同理,在非Controller层的异常处理器中,我们使用@HandlerAdvice来标注非Web层的异常处理类,使用@ExceptionAcceptor注解标注在方法上。

小汪:那自动配置类、spring.factories文件中声明,是怎么做的?

大榜:自动配置类ExceptionHandlerAutoConfiguration,代码如下:

package com.mfy.exception;
​
import com.mfy.exception.resolver.ExceptionAcceptorExceptionResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
​
/**
 * 异常处理器的自动配置类
 */
@Configuration
@ConditionalOnClass(value = {ExceptionAcceptorExceptionResolver.class})
public class ExceptionHandlerAutoConfiguration {
​
    @Autowired
    private Environment environment;
​
    @Bean("exceptionAcceptorExceptionResolver")
    @ConditionalOnMissingBean(ExceptionAcceptorExceptionResolver.class) // 若容器中不存在ExceptionAcceptorExceptionResolver对象,才会执行下面的方法
    public ExceptionAcceptorExceptionResolver getExceptionResolver(){
        String property = environment.getProperty("rabbitmq.exception.throw");
        ExceptionAcceptorExceptionResolver exceptionResolver = new ExceptionAcceptorExceptionResolver();
        exceptionResolver.setFlag(Boolean.parseBoolean(property));
        return exceptionResolver;
    }
}

spring.factories文件中声明该自动配置类,如下:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.mfy.exception.ExceptionHandlerAutoConfiguration

小汪:那自动配置类中,如何实现非Controller层的异常捕获的呢,代码在哪里?

大榜:异常处理的核心逻辑全都在ExceptionAcceptorExceptionResolver类中,自动配置类将ExceptionAcceptorExceptionResolver对象加入到IOC容器中,系统启动时就可以直接使用了。核心逻辑的代码放在仓库上:github.com/maofangyun/…

小汪:你的非Controller层的全局异常处理器编写和测试通过后,我怎么使用它呢?

大榜:这很简单,你只需要引入异常处理器的pom依赖,然后使用@HandlerAdvice来标注非Controller层的异常处理类,使用@ExceptionAcceptor注解标注在捕获方法上,就可以了。代码如下:

package com.cloud.occultation.exception;
​
import com.cloud.common.entity.exception.BaseException;
import com.mfy.exception.annotation.ExceptionAcceptor;
import com.mfy.exception.annotation.HandlerAdvice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
​
/**
 * 全局异常处理器
 */
@HandlerAdvice
@Slf4j
public class GlobalExceptionHandler {
​
    @ExceptionAcceptor(value = {ProtocolParseException.class, NullPointerException.class})
    public void handlerException(ProtocolParseException protocolParseException){
        // 堆栈信息和错误码记录日志
        log.error(protocolParseException.getLocation(),protocolParseException);
    }
​
    @ExceptionAcceptor(value = BaseException.class)
    public void handlerException(BaseException baseException){
        // 堆栈信息和错误码记录日志
        log.error(baseException.getLocation(),baseException);
    }
​
    // 设置兜底异常,当业务逻辑抛出的异常无法与上面的异常类型相匹配时,会调用此处的异常捕获方法
    @ExceptionAcceptor(value = Exception.class)
    public void handlerException(Exception exception, Throwable throwable){
        // 堆栈信息和错误码记录日志
        log.error("兜底异常,未知错误:",exception);
    }
​
}

上面的代码中,我们设置了兜底异常,当业务逻辑抛出的异常无法与上面的异常类型相匹配时,会调用此处的异常捕获方法handlerException(Exception exception, Throwable throwable)。

小汪:我懂了。我按照你刚刚讲的,编写了一个测试类:

package com.cloud.occultation.web.demo;
​
import com.cloud.common.entity.exception.BaseException;
import com.mfy.exception.resolver.ExceptionAcceptorExceptionResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
import java.io.UnsupportedEncodingException;
​
/**
 * @Date 2022/7/14 11:34
 */
@Slf4j
@RestController
@RequestMapping("/")
public class GlobalExceptionDemo {
​
    @Autowired
    private RabbitTemplate rabbitTemplate;
​
    @Autowired
    private ExceptionAcceptorExceptionResolver exceptionResolver;
​
    @GetMapping("/hello")
    public String hello() throws UnsupportedEncodingException {
        String str = "123";
        Message message = MessageBuilder.withBody(str.getBytes("UTF-8")).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
​
        try {
            rabbitTemplate.send(message);
//            throw new BaseException();
            throw new IllegalStateException("非法状态异常来了!");
        } catch (Exception e) {
            exceptionResolver.doResolveException(e);
        }
​
        return "hello888";
​
    }
}

输出结果为:当业务逻辑抛出new BaseException()时,会被异常处理类中的handlerException(BaseException baseException)方法捕获;而当业务抛出new IllegalStateException("非法状态异常来了!")时,由于IllegalStateException与异常处理类中的其他类不匹配,所以最终会被兜底方法捕获,即被handlerException(Exception exception, Throwable throwable)捕获并处理。是这样吧?

2.3、分布式锁的starter

大榜:你理解得很对,是块写代码的好材料,哈哈哈。接下来我们讨论分布式锁的starter场景启动器。分布式锁是相对于单机版锁而言的,它的作用是对共享内存中的资源进行加锁访问,防止多个进程同一时间操作此资源而导致的线程不安全。对于分布式锁的介绍可以参考拙作:Redis的分布式锁

小汪:那分布式锁的starter场景启动器,如何实现呢?应该也需要在spring.factories文件中,添加一个自动配置类,这样Spring Boot启动的时候就会去加载该自动配置类了。

大榜:是滴了,都是一样的套路,spring.factories文件中,编写的自动配置类如下所示:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.mfy.lock.LockAutoConfiguration

接下来,在LockAutoConfiguration自动配置类中,将相关组件加入到IOC容器

package com.mfy.lock;
​
import com.mfy.lock.advisor.LockAdvice;
import com.mfy.lock.advisor.LockAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
​
/**
 * 分布式锁的自动配置类:LockAutoConfiguration
 *
 * @date 2021/9/6 14:35
 */
@Configuration
@ComponentScan("com.mfy.lock")
public class LockAutoConfiguration {
​
    @Bean
    public LockAdvisor lockAdvisor(LockAdvice lockAdvice){
        LockAdvisor lockAdvisor = new LockAdvisor();
        lockAdvisor.setAdvice(lockAdvice);
        return lockAdvisor;
    }
​
    @Bean
    public LockAdvice lockAdvice(){
        return new LockAdvice();
    }
​
}

小汪:如果我需要使用Redis使用分布式锁,该如何实现呢?

大榜:我们可以使用set、setnx、expire指令来实现分布式锁,具体来说:在Redis中创建一个key,这个key有一个失效时间(TTL),以保证锁最终会被自动释放掉。当客户端释放资源(解锁)的时候,会删除掉这个key。但当Redis以哨兵或集群部署时,这种方法会存在主从问题,也就是当主节点挂掉的时候,故障转移期间可能会导致多个客户端同时访问同一个共享资源,不安全性由此产生。于是Redis引入了红锁RedLock算法,redisson库就是红锁算法的具体实现。

小汪:既然set、setnx、expire指令来实现分布式锁,会因为主从切换问题导致不安全。那使用redission来实现分布式锁应该就可以了把?

大榜:是滴了,redisson是Redis客户端,它采用主节点过半机制,也就是获取锁或者释放锁成功的标志为 在过半的节点上操作成功,来保证主从切换不会造成安全问题。业界一般使用redission来实现分布式锁,代码是下面这样的:

package com.mfy.lock.component;
​
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @date 2021/9/6 16:01
 */
@Component
@ConditionalOnProperty(value = "spring.redis.host") // 只有当项目中存在该配置项"spring.redis.host"时,才会将RedisLock加入到IOC容器
public class RedisLock implements Lock, InitializingBean {
​
    @Autowired
    private Environment environment;
​
    private RedissonClient redissonClient;
​
    private RLock lock;
​
    @Override
    public String getName() {
        return "redisLock";
    }
​
    @Override
    public void lock(long leaseTime, TimeUnit unit, String key) throws Exception{
        lock = redissonClient.getLock(key); // 给锁对象起个名字,保证锁对象lock 的唯一性,避免锁对象冲突
        lock.lock(leaseTime,unit);
    }
​
    @Override
    public void unlock() {
        if(lock.isLocked() && lock.isHeldByCurrentThread()){
            lock.unlock();
        }
    }
​
    @Override
    public void afterPropertiesSet() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + environment.getProperty("spring.redis.host") + ":" + environment.getProperty("spring.redis.port"));
        config.useSingleServer().setPassword(environment.getProperty("spring.redis.password"));
        config.useSingleServer().setConnectionPoolSize(Integer.valueOf(environment.getProperty("spring.redis.lettuce.pool.max-active")));
        config.useSingleServer().setConnectionMinimumIdleSize(Integer.valueOf(environment.getProperty("spring.redis.lettuce.pool.max-idle")));
        redissonClient = Redisson.create(config);
    }
​
}

上面的代码中,redissonClient来获取锁、释放锁,并且给锁设置过期时间,这样可以避免死锁。

小汪:榜哥,如果我想要使用你编写的分布式锁RedisLock,该如何使用呢?

大榜:很简单,你只需要使用@Autowired注解,将RedisLock对象装配到你的项目,然后手动调用lock、unlock就可以了,代码如下:

   @Autowired
   private RedisLock myLock;
​
    private void test() {
        Random random = new Random();
        int value = random.nextInt(100);
​
        try {
            myLock.lock(20, TimeUnit.SECONDS, "sayHello");
            log.info("加锁成功:{}", value);
        } catch (Exception e) {
            log.error("加锁失败", e);
        } finally {
            myLock.unlock();
            log.info("释放锁:{}", value);
        }
​
    }

分布式锁的starter场景启动器,源码如下:github.com/maofangyun/…

你直接下载就可以拿来用了。

3、总结

通过小汪和大榜的对话,我们一起讨论了Spring 创建bean对象的生命周期,以及对Aware接口、bean初始化及销毁过程的扩展功能进行了实战。接着讲解了Spring Boot中用的最多的机制:starter场景启动器,并实现了自定义的场景启动器。

4、参考内容

JavaGuide-如何实现一个starter