Spring踩坑一

《Spring编程常见错误50例》读书笔记

源码构建依赖下载可能遇到的问题:

https://blog.csdn.net/xiangxiaotian666/article/details/127399904

1671546630072-cba60336-a06d-47a4-a6df-fbd2d6086c9d.png

Spring Core

  1. 工厂 BeanFactory 内部有 Map
  2. Scan 负责扫描需要实例化的 Bean
  3. 最终使用反射的方式实例化:
    1. java.lang.Class.newInsance();
    2. java.lang.reflect.Constructor.newInstance();
    3. ReflectionFactory.newConstructorForSerialization();
  4. AOP 使用多态代理实现,需要代理哪些类就使用 @Aspect 中定义的 切点 来表示

Bean 定义

隐式扫描不到 Bean 的定义

:::color4
定义的类没有写到 Application 同级或者下级目录的时候就找不到 Bean 定义

:::

解决

可以在 Application 上写上:

  • @ComponentScans(value = { @ComponentScan(value ="com.spring.puzzle.class1.example1.controller") })-
  • @ComponentScan可以多个同时使用,且都生效。效果等同于@ComponentScans
  • @SpringBootApplication(scanBasePackages = {"com.xxx.xxxxx","com.xxx.xxx"})

原理

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { 
    @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非关键代码
}

Spring 默认规则:ComponentScan 注解的 basePackages 属性指定扫描哪些 Bean 定义。没有指定,默认为空(即{})。此时解析启动类(配置类)的时候,就会为其填充默认值:

画板

定义的 Bean 缺少隐式依赖

:::color4
自定义了构造器,但是没有将构造器的入参注入到 spring,导致 spring 使用对应的构造器实例化 Bean 时候找不到参数而失败

:::

@Service
public class ServiceImpl {
    private String serviceName;
    public ServiceImpl(String serviceName){
        this.serviceName = serviceName;
    }
}

解决

定义一个Bean,名字就是 serviceName,这个bean装配给ServiceImpl的构造器参数“serviceName”

@Bean
public String serviceName(){
    return "MyServiceName";
}

原理

我们定义一个类为 Spring Bean A,如果再显式定义了构造器,那么这个 Bean A 在构建时,会自动根据构造器参数定义寻找对应类型的 Bean B,然后反射创建出这个 Bean B 作为 A 构造器的入参去反射创建 Bean A。

画板

自定义多个构造器没有指定优先级

存在两个构造器,都可以调用时,最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。

@Service
public class ServiceImpl {
    private String serviceName;
    public ServiceImpl(String serviceName){
        this.serviceName = serviceName;
    }
    public ServiceImpl(String serviceName, String otherStringParameter){
        this.serviceName = serviceName;
    }
}

原型Bean不能如你所愿每次都创建

:::color4
定义了一个原型的 Bean A,在另外一个 Bean B 中使用 @Autowired 注入之后,希望每次调用都是一个新的对象,但是其实是同一个对象。因为 Bean B 其实只实例化了一次,A 作为被依赖方只有在 B 实例化的时候被调用

:::

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
@RestController
public class HelloWorldController {

    @Autowired
    private ServiceImpl serviceImpl;

    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld, service is : " + serviceImpl;
    };
}

访问多少次,结果都是一样的

解决

不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。

方式一:每次从 ApplicationContext 中获取
@RestController
public class HelloWorldController {
    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
        return "helloworld, service is : " + getServiceImpl();
    };

    public ServiceImpl getServiceImpl(){
        return applicationContext.getBean(ServiceImpl.class);
    }
}
方式二: 使用 @Lookup
@RestController
public class HelloWorldController {
 
    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
        return "helloworld, service is : " + getServiceImpl();
    };

    @Lookup
    public ServiceImpl getServiceImpl(){
        return null;
    }
}

https://blog.csdn.net/qq_25863845/article/details/123475147

标记了 Lookup 的方法最终走入了CGLIB 搞出的类 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept方法调用最终并没有走入案例代码实现的return null语句,而是通过 BeanFactory 来获取 Bean。其实 **getServiceImpl** 方法实现中,随便怎么写都行。

为什么会使用cgLib?是因为在初始化实例的时候(参考 SimpleInstantiationStrategy#instantiate),会发现有方法标记了 Lookup,此时就会添加相应方法到属性methodOverrides 里面去(此过程由 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成),Lookup 会重写方法。也就是说方法内部的实现,在Bean被实例化的时候就被替换成 CGLIB 的实现了。

@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
    // 当 hasMethodOverrides 为 true 时,则使用 CGLIB。
    if (!bd.hasMethodOverrides()) {
        //
        return BeanUtils.instantiateClass(constructorToUse);
    }
    else {
        // Must generate CGLIB subclass.
        return instantiateWithMethodInjection(bd, beanName, owner);
    }
}

原理

找到要自动注入的 Bean 后,通过反射设置给对应的field。这个field的执行只发生了一次,所以后续就固定起来了,并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。所以我们请求多少次都不会重新去Spring容器获取 ServiceImpl,而是直接获取 Controller 引用的那个对象。

画板

注入

required a single bean, but 2 were found

:::danger
@Autowired 的一个接口有多个实现的时候,就无法工作了

:::

@RestController
@Slf4j
@Validated
public class StudentController {
    @Autowired
    DataService dataService;

    @RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
    public void deleteStudent(@PathVariable("id") @Range(min = 1,max = 100) int id){
        dataService.deleteStudent(id);
    };
}
public interface DataService {
    void deleteStudent(int id);
}

@Repository
@Slf4j
public class OracleDataService implements DataService{
    @Override
    public void deleteStudent(int id) {
        log.info("delete student info maintained by oracle");
    }
}

@Repository
@Slf4j
public class CassandraDataService implements DataService{
    @Override
    public void deleteStudent(int id) {
        log.info("delete student info maintained by cassandra");
    }
}

解决

让候选实现类具有优先级或压根可以不去选择。

方式一:设置优先级
@Repository
@Primary
@Slf4j
public class OracleDataService implements DataService{
    //省略非关键代码
}
方式二:指定精确 Bean名字

将属性名和Bean名字精确匹配,这样就可以让注入选择不犯难

@Autowired
DataService oracleDataService;
方式三:使用 @Qualifier 显式指定使用哪种
@Autowired()
@Qualifier("cassandraDataService")
DataService dataService;
方式四:使用策略模式存到自己定义的 map 中

自己定义一个策略类,实现 InitializingBean� 的 afterPropertiesSet� 之后,将这些实现类注入到自己的 map 中,使用时候获取对应类型的实现即可。

原理

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
      //省略非关键代码
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
         if (bp instanceof InstantiationAwareBeanPostProcessor) {
            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
            PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
          //省略非关键代码
         }
      }
   }
}

因为StudentController含有标记为@Autowired的成员属性dataService,所以会使用到AutowiredAnnotationBeanPostProcessor(BeanPostProcessor中的一种)来完成依赖注入。

回看上面讲过的依赖注入的流程,图中粉色就是寻找依赖的关键:

画板

寻找时候如果有多个实现的时候,具体的逻辑如下:

画板

可以看到,有多个实现,又没设置优先级,又没有可以精确匹配 BeanName 的属性名,又不是数组等类型,就会报错。

显式引用Bean时首字母忽略大小写

:::danger
Qualifier 中 Bean 的名称首字母大写报错:

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'studentController': Unsatisfied dependency expressed through field 'dataService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.spring.puzzle.class2.example2.DataService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=CassandraDataService)}

:::

对于实现类 CassandraDataService ,下面的定义会报错

@Autowired
@Qualifier("CassandraDataService")
DataService dataService;

但是首字母小写之后就不会报错了。

但是对于实现类 SQLiteDataService,首字母小写反而报错了:

@Autowired
@Qualifier("sQLiteDataService")
DataService dataService;

如果改成SQLiteDataService,则运行通过了。

解决

方式一:引用处纠正首字母大小写问题
@Autowired
@Qualifier("cassandraDataService")
DataService dataService;
方式二:定义处显式指定Bean名字
@Repository("CassandraDataService")
@Slf4j
public class CassandraDataService implements DataService {
    //省略实现
}

原理

画板

当没有指定 Bean 的名字的时候,默认的名字生成规则就是:

先获取简单类名,如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写。

public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                    Character.isUpperCase(name.charAt(0))){
        return name;
    }
    char chars[] = name.toCharArray();
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}

依赖注入内部类的 Bean 找不到 Bean

:::danger
对于内部类,想要作为Bean注入,指定的 beanName 名如果按照普通类来写,仍然会报错“找不到Bean”

:::

public class StudentController {
    @Repository
    public static class InnerClassDataService implements DataService{
        @Override
        public void deleteStudent(int id) {
            //空实现
        }
    }
    //省略其他非关键代码
}
@Autowired
@Qualifier("innerClassDataService")
DataService innerClassDataService;

解决

@Autowired
@Qualifier("studentController.InnerClassDataService")
DataService innerClassDataService;

原理

AnnotationBeanNameGenerator#buildDefaultBeanName 获取简短类名的时候 String shortClassName = ClassUtils.getShortName(beanClassName);

public static String getShortName(String className) {
    Assert.hasLength(className, "Class name must not be empty");
    // 只是用包名来切割 PACKAGE_SEPARATOR
    int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
    int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
    if (nameEndIndex == -1) {
        nameEndIndex = className.length();
    }
    String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
    shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR);
    return shortName;
}

如果类名是 com.spring.puzzle.class2.example3.StudentController.InnerClassDataService,那么处理之后就是 StudentController.InnerClassDataService,经过Introspector.decapitalize的首字母变换,最终获取的Bean名称如下:**studentController.InnerClassDataService**。直接使用 innerClassDataService 自然找不到想要的Bean。

内部类类名:com.spring.puzzle.class2.example3.StudentController.InnerClassDataService
应该是:com.spring.puzzle.class2.example3.StudentController$InnerClassDataService, "."换成"$"

@Value 基础知识:

使用@Autowired一般都不会设置属性值,而@Value必须指定一个字符串值,因为其定义做了要求。我们一般都会因为@Value常用于String类型的装配而误以为@Value不能用于非内置对象的装配,实际上这是一个常见的误区

@Bean
public Student student(){
    Student student = createStudent(1, "xie");
    return student;
}
//注册正常字符串
@Value("我是字符串")
private String text;

//注入系统参数、环境变量或者配置文件中的值
@Value("${ip}")
private String ip

@Value("#{student}")
private Student student;

//注入其他Bean属性,其中student为bean的ID,name为其属性
@Value("#{student.name}")
private String name;

@Value没有注入预期的值

:::danger
在配置文件application.properties配置了这样一个属性:

username=admin

password=pass

然后我们在一个Bean中,分别定义两个属性来引用它们:

@Value("${username}")

private String username;

@Value("${password}")

private String password;

password正确返回了,username返回的并不是配置文件中指明的admin,而是运行这段程序的计算机用户名。

:::

解决

user.name=admin
user.password=pass

在systemProperties这个PropertiesPropertySource源中刚好存在user.name,所以命名时,我们一定要注意 不仅要避免和环境变量冲突,也要注意避免和系统变量等其他变量冲突

原理

画板

[ConfigurationPropertySourcesPropertySource {name='configurationProperties'},
StubPropertySource {name='servletConfigInitParams'}, ServletContextPropertySource {name='servletContextInitParams'}, PropertiesPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'},
OriginTrackedMapPropertySource {name='applicationConfig: classpath:/application.properties]'},
MapPropertySource {name='devtools'}]

具体的查找执行,我们可以通过下面的代码(PropertySourcesPropertyResolver#getProperty)来获取它的执行方式:

@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    if (this.propertySources != null) {
        for (PropertySource<?> propertySource : this.propertySources) {
            Object value = propertySource.getProperty(key);
            if (value != null) {
                //查到value即退出
                return convertValueIfNecessary(value, targetValueType);
            }
        }
    }

    return null;
}

在解析Value字符串时,其实是有顺序的(查找的源是存在CopyOnWriteArrayList中,在启动时就被有序固定下来),一个一个“源”执行查找,在其中一个源找到后,就可以直接返回了。如果我们查看systemEnvironment这个源,会发现刚好有一个username和我们是重合的,且值不是pass。刚好系统环境变量(systemEnvironment)中含有同名的配置。实际上,对于系统参数(systemProperties)也是一样的,这些参数或者变量都有很多,如果我们没有意识到它的存在,起了一个同名的字符串作为@Value的值,则很容易引发这类问题。

注入的集合被覆盖

@Bean
public Student student1(){
    Student student = new Student();
    student.setId(1);
    student.setName("111");
    return student;
}

@Bean
public Student student2(){
    Student student = new Student();
    student.setId(2);
    student.setName("222");
    return student;
}

@Bean
public List<Student> students(){
    Student student3 = createStudent(3, "333");
    Student student4 = createStudent(4, "444");
    return Arrays.asList(student3, student4);
}

beanName 为 student1 和 student2 的 Student 对象

beanName 为 students 的 List<Student> 对象

@RestController
@Slf4j
public class StudentController {

    private List<Student> students;

    public StudentController(List<Student> students){
        this.students = students;
    }

    @RequestMapping(path = "students", method = RequestMethod.GET)
    public String listStudents(){
        return students.toString();
    };

}

通过上述代码,我们就可以完成集合类型的注入工作,输出结果如下:

[Student(id=1, name=111), Student(id=2, name=222)]

解决

@Bean
public List<Student> students(){
    Student student1 = createStudent(1, "xie");
    Student student2 = createStudent(2, "fang");
    Student student3 = createStudent(3, "liu");
    Student student4 = createStudent(4, "fu");
    return Arrays.asList(student1,student2,student3, student4);
}

或者

@Bean
public Student student1(){
    return createStudent(1, "xie");
}
@Bean
public Student student2(){
    return createStudent(2, "fang");
}
@Bean
public Student student3(){
    return createStudent(3, "liu");
}
@Bean
public Student student4(){
    return createStudent(4, "fu");
}

@Order 、@Priority注解指定顺序

数值越小,优先级越高

原理

发现上面的 private List students; 作为构造器的入参,此时就会去找对应的 component ,由于是 List 集合类型。所以在执行doResolveDependency依赖注入的时候,首先使用 DefaultListableBeanFactory#resolveMultipleBeans来尝试完成对集合的装配工作:

private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
                                    @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
    final Class<?> type = descriptor.getDependencyType();
    if (descriptor instanceof StreamDependencyDescriptor) {
        //装配stream
        return stream;
    }
    else if (type.isArray()) {
        //装配数组
        return result;
    }
    else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
        //装配集合
        //获取集合的元素类型
        Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
        if (elementType == null) {
            return null;
        }
        //根据元素类型查找所有的bean
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
                                                                   new MultiElementDescriptor(descriptor));
        if (matchingBeans.isEmpty()) {
            return null;
        }
        if (autowiredBeanNames != null) {
            autowiredBeanNames.addAll(matchingBeans.keySet());
        }
        //转化查到的所有bean放置到集合并返回
        TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
        Object result = converter.convertIfNecessary(matchingBeans.values(), type);
        //省略非关键代码
        return result;
    }
    else if (Map.class == type) {
        //解析map
        return matchingBeans;
    }
    else {
        return null;
    }
}

1. 获取集合类型的元素类型

2. 根据元素类型,找出所有的Bean

3. 将匹配的所有的Bean按目标类型进行转化。经过步骤2,我们获取的所有的Bean都是以java.util.LinkedHashMap.LinkedValues形式存储的,和我们的目标类型大概率不同,所以最后一步需要做的是 按需转化。深究执行细节,就可以知道最终是转化器CollectionToCollectionConverter来完成这个转化过程。

当上面的方式没有找到任何的实现类的时候,才会使用beanName 来寻找。代码如下:

Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
    return multipleBeans;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);

所以直接 return 了,就不会去找以 students 为 beanName 的 component 了。

Spring Bean 生命周期

构造器内抛空指针异常

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
    @Autowired
    private LightService lightService;

    // 构造器
    public LightMgrService() {
        lightService.check();
    }
}

NullPointerException

解决

方式一:构造器的隐式注入

**@Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的 **。

@Component
public class LightMgrService {

    private LightService lightService;

    public LightMgrService(LightService lightService) {
        this.lightService = lightService;
        lightService.check();
    }
}

构造器参数 LightService 会被自动注入LightService 的 Bean,从而在构造器执行时,不会出现空指针。可以说, 使用构造器参数来隐式注入是一种 Spring 最佳实践

方式二:@PostConstruct 或者实现 InitializingBean 接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
    @Autowired
    private LightService lightService;

    @PostConstruct
    public void init() {
        lightService.check();
    }
}
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService implements InitializingBean {
    @Autowired
    private LightService lightService;

    @Override
    public void afterPropertiesSet() throws Exception {
        lightService.check();
    }
}

原理

1671717595125-a5d6afdf-3481-4b9f-909e-4adb20c81255.png

  • 环境准备:将一些必要的系统类的定义,比如 Bean 的后置处理器类,注册到 Spring 容器。然后将这些后置处理器实例化,并注册到 Spring 的容器中。
    1. 很多必要的系统类,尤其是 Bean 后置处理器(比如CommonAnnotationBeanPostProcessorAutowiredAnnotationBeanPostProcessor 等),Spring 能够非常灵活地在不同的场景调用不同的后置处理器,比如 @PostConstruct 注解,它的处理逻辑就需要用到 CommonAnnotationBeanPostProcessor(继承自 InitDestroyAnnotationBeanPostProcessor)这个后置处理器。
  • 实例化用户定义的Bean:实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等等。

画板

意外触发 shutdown

:::danger
Spring 容器关闭的时候:

  • 会调用 @Bean 的方式注入的Bean的 shutdown 方法
  • 而使用 @Component 注入到 Spring 容器时,shutdown 方法则不会被自动执行。

:::

import org.springframework.stereotype.Service;
@Service
public class LightService {
    //省略其他非关键代码
    public void shutdown(){
        System.out.println("shutting down all lights");
    }
    //省略其他非关键代码
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
    @Bean
    public LightService getTransmission(){
        return new LightService();
    }
}

让 Spring 启动完成后立马关闭当前 Spring 上下文

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
        context.close();
    }
}

实际运行这段代码后,我们可以看到控制台上打印了 shutting down all lights。显然 shutdown 方法未按照预期被执行了。

解决

避免在Java类中定义一些带有特殊意义动词的方法来解决,当然如果一定要定义名为 close 或者 shutdown 方法,也可以通过将 Bean 注解内 destroyMethod 属性设置为空的方式来解决这个问题。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
    @Bean(destroyMethod="")
    public LightService getTransmission(){
        return new LightService();
    }
}

原理

1671771420383-d3584aed-1235-4893-8116-fe39a0007f98.png

从注解的源码可以看到:使用 **@Bean** 注解注册的 Bean 对象,如果不设置 destroyMethod 属性,则其属性默认值为 AbstractBeanDefinition.INFER_METHOD。查找 INFER_METHOD 枚举值的引用,很容易就找到了使用该枚举值的方法 DisposableBeanAdapter#inferDestroyMethodIfNecessary

1671778561120-86b1eddc-e114-4766-9b9b-84dd10107459.png

destroyMethodName 属性的名字如果等于 INFER_METHOD,且当前类没有实现 DisposableBean 接口,那么首先查找类的 close 方法,如果找不到,就在抛出异常后继续查找 shutdown 方法;如果找到了,则返回其方法名(close 或者 shutdown)。

如果继续查找是谁引用了这个方法,就可以得到下面的流程图:

画板

为什么 @Service 注入的 Bean,其 shutdown 方法不能被执行?想要执行,则必须要添加 DisposableBeanAdapter,而它的添加是有条件的(AbstractBeanFactory#registerDisposableBeanIfNecessary):

1671780549680-172aa198-2fc1-4fae-9820-deb6e31d8d5a.png

很明显,在案例代码修改前后,我们都是单例,所以区别仅在于是否满足requiresDestruction 条件。翻阅它的代码,最终的关键调用参考DisposableBeanAdapter#hasDestroyMethod:

1671780808208-e5b8808d-4ede-47b3-ae98-b6d9eeda571e.png

如果我们是使用 @Service 来产生 Bean 的,那么在上述代码中我们获取的destroyMethodName 其实是 null;而使用 @Bean 的方式,默认值为AbstractBeanDefinition.INFER_METHOD,使用 @Service 的 Bean 也没有实现 AutoCloseable、DisposableBean,最终没有添加一个 DisposableBeanAdapter。所以最终我们定义的 shutdown 方法没有被调用。

根据上面的,只要我们实现 DisposableBean�� 或 AutoCloseable� 也是可以被自动调用方法的。

AOP Aspect Oriented Programming

Spring AOP则利用CGlib和JDK动态代理等方式来实现运行期动态方法增强,JDK动态代理只能对实现了接口的类生成代理,而不能针对普通类。而CGLIB是可以针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,来实现代理对象。

其目的是将与业务无关的代码单独抽离出来,使其逻辑不再与业务代码耦合,从而降低系统的耦合性,提高程序的可重用性和开发效率。因而AOP便成为了日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用的技术。

在Spring Boot中,可以使用 spring-boot-starter-aop 依赖开启。而对于非Spring Boot程序,除了添加相关AOP依赖项外,还会使用@EnableAspectJAutoProxy来开启AOP功能。这个注解类引入(Import)AspectJAutoProxyRegistrar,它通过实现ImportBeanDefinitionRegistrar的接口方法来完成AOP相关Bean的准备工作。

this调用的当前类方法无法被拦截

:::danger
**在@Around的切面类中定义了切面对应的方法,但是却没有被执行到。这说明在类的内部,通过this方式调用的方法,是没有被Spring AOP增强的。 **

:::

@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();
    }

    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}
@Aspect
@Service
@Slf4j
public class AopConfig {
    @Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ")
    public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("Pay method time cost(ms): " + (end - start));
    }
}

解决

方式一:在自己里面注入自己

只有引用的是被动态代理创建出来的对象,才会被Spring增强,具备AOP该有的功能。通过@Autowired的方式,在类的内部,自己引用自己:

@Service
public class ElectricService {
    
    @Autowired
    ElectricService electricService;
    
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        //this.pay();
        electricService.pay();
    }
    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}
方式二:从 AopContext 获取代理对象

AopContext 就是通过一个 ThreadLocal 来将 Proxy 和线程绑定起来,这样就可以随时拿出当前线程绑定的Proxy。前提是需要在@EnableAspectJAutoProxy里加一个配置项exposeProxy = true,表示将代理对象放入到ThreadLocal,这样才可以直接通过 AopContext.currentProxy()的方式获取到,否则会报错如下:

1671785202861-25cb7dcb-9271-4244-a0dd-a19e4b4ab015.png

import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        ElectricService electric = ((ElectricService) AopContext.currentProxy());
        electric.pay();
    }
    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
    // 省略非关键代码java
}

原理

Controller层中自动装配的ElectricService对象是一个被Spring增强过的Bean,所以执行charge()方法时,会执行记录接口调用时间的增强操作。而this对应的对象只是一个普通的对象,并没有做任何额外的增强。

创建代理对象的时机就是创建一个Bean的时候,而创建的的关键工作其实是由AnnotationAwareAspectJAutoProxyCreator完成的。它本质上是一种BeanPostProcessor。它的执行是在完成原始Bean构建后的初始化Bean(initializeBean)过程中。

画板

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
                             @Nullable Object[] specificInterceptors, TargetSource targetSource) {
    // 省略非关键代码

    // 创建一个代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    if (!proxyFactory.isProxyTargetClass()) {
        if (shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
        }
        else {
            evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }
    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    // 将通知器(advisors)、被代理对象等信息加入到代理工厂
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);
    // 省略非关键代码

    // 通过这个代理工厂来获取代理对象
    return proxyFactory.getProxy(getProxyClassLoader());
}

直接访问被拦截类的属性抛空指针异常

public class User {
    private String payNum;
    public User(String payNum) {
        this.payNum = payNum;
    }
    public String getPayNum() {
        return payNum;
    }
    public void setPayNum(String payNum) {
        this.payNum = payNum;
    }
}
@Service
public class AdminUserService {
    public final User adminUser = new User("202101166");

    public void login() {
        System.out.println("admin user login...");
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ElectricService {
    
    @Autowired
    private AdminUserService adminUserService;
    
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();
    }

    public void pay() throws Exception {
        adminUserService.login();
        String payNum = adminUserService.adminUser.getPayNum();
        System.out.println("User pay num : " + payNum);
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}

:::danger
String payNum = dminUserService.user.getPayNum();

本来一切正常的代码,因为引入了一个AOP切面,抛出了NullPointerException。

:::

@Aspect
@Service
@Slf4j
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) ")
    public void logAdminLogin(JoinPoint pjp) throws Throwable {
        System.out.println("! admin login ...");
    }
}

解决

**一般不能直接从代理类中去拿被代理类的属性,除非我们显示设置spring.objenesis.ignore为true,否则代理类的属性是不会被Spring初始化的,我们可以通过在被代理类中增加一个方法来间接获取其属性。 **

方式一

UserService里写个getUser()方法,从内部访问获取变量,此时用就不是 adminUserService 里面的变量值,而是真正的被代理对象里的属性值

public User getUser() {
    return user;
}

在ElectricService里通过getUser()获取User对象:

//原来出错的方式:
String payNum = = adminUserService.adminUser.getPayNum();
//修改后的方式:
String payNum = adminUserService.getAdminUser().getPayNum();
方式二

修改启动参数spring.objenesis.ignore如下:

1671878424730-5fe0031e-7aff-4558-bc14-41dc80c3f3b5.png

此时再调试程序,你会发现adminUser已经不为null了。

原理

正常情况下,AdminUserService 只是一个普通的对象,而AOP增强过的则是一个AdminUserService $$EnhancerBySpringCGLIB$$xxxx。是AdminUserService的一个子类。它会overwrite所有public和protected方法,并在内部将调用委托给原始的AdminUserService实例。

从具体实现角度看,Spring的动态代理对象的初始化机制就是在得到Advisors之后:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
                             @Nullable Object[] specificInterceptors, TargetSource targetSource) {
    // 省略非关键代码

    // 创建一个代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    if (!proxyFactory.isProxyTargetClass()) {
        if (shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
        }
        else {
            evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }
    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    // 将通知器(advisors)、被代理对象等信息加入到代理工厂
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);
    // 省略非关键代码

    // 通过这个代理工厂来获取代理对象
    return proxyFactory.getProxy(getProxyClassLoader());
}

会通过ProxyFactory.getProxy获取代理对象。以CGLIB的Proxy的实现类CglibAopProxy为例,CGLIB中AOP的实现是基于org.springframework.cglib.proxy包中 EnhancerMethodInterceptor两个接口来实现的:

public Object getProxy(ClassLoader classLoader) {
    return createAopProxy().getProxy(classLoader);
}

在这里,我们以CGLIB的Proxy的实现类CglibAopProxy为例,CglibAopProxy#getProxy 都会执行到CglibAopProxy子类ObjenesisCglibAopProxy#createProxyClassAndInstance方法:

public Object getProxy(@Nullable ClassLoader classLoader) {
    // 省略非关键代码
    // 创建及配置 Enhancer,
    Enhancer enhancer = createEnhancer();
    // 省略非关键代码
    // 获取Callback:包含DynamicAdvisedInterceptor,亦是MethodInterceptor
    // MethodInterceptor 负责委托方法执行;
    Callback[] callbacks = getCallbacks(rootClass);
    // 省略非关键代码
    // 生成代理对象并创建代理(设置 enhancer 的 callback 值)
    return createProxyClassAndInstance(enhancer, callbacks);
    // 省略非关键代码
}

Spring 会默认尝试使用 objenesis 方式实例化对象,如果失败则再次尝试使用常规方式实例化对象。objenesis方式最后使用了JDK的 ReflectionFactory.newConstructorForSerialization() 完成了代理对象的实例化这种方式创建出来的对象是不会初始化类成员变量的。但是下面两种通过反射来实例化对象的方式都会同时初始化类成员变量:

  • java.lang.Class.newInsance()
  • java.lang.reflect.Constructor.newInstance()

代理类的类属性不会被初始化,那为什么可以通过在AdminUserService里写个getUser()方法来获取代理类实例的属性呢?createProxyClassAndInstance的代码逻辑,创建代理类后,我们会调用setCallbacks来设置拦截后需要注入的代码:

protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    Class<?> proxyClass = enhancer.createClass();
    Object proxyInstance = null;
    if (objenesis.isWorthTrying()) {
        try {
            proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
        }
        // 省略非关键代码
        ((Factory) proxyInstance).setCallbacks(callbacks);
        return proxyInstance;
    }

上述的callbacks中会存在一种服务于AOP的**DynamicAdvisedInterceptor**,它的接口是MethodInterceptor(callback的子接口),实现了拦截方法intercept()

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    // 省略非关键代码
    TargetSource targetSource = this.advised.getTargetSource();
    // 省略非关键代码
    target = targetSource.getTarget();
    // 省略非关键代码
    if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        retVal = methodProxy.invoke(target, argsToUse);
    }
    else {
        // We need to create a method invocation...
        retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
}
//省略非关键代码

当代理类方法被调用,会被Spring拦截,从而进入此intercept(),并在此方法中获取被代理的原始对象。而在原始对象中,类属性是被实例化过且存在的。因此代理类是可以通过方法拦截获取被代理对象实例的属性。

错乱混合不同类型的增强

@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
    }
}
//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        System.out.println("validating user authority");
        Thread.sleep(1000);
    }

    @Around("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println("charge method time cost: " + (end - start));
    }
}

:::color4
执行后得到日志如下:

我们的初衷仅仅是 ElectricService.charge() 的性能统计,它并不包含鉴权过程。

:::

validating user authority

Electric charging …

charge method time cost 1022 (ms)

解决

@Service
public class ElectricService {
    @Autowired
    ElectricService electricService;
    
    public void charge() {
        electricService.doCharge();
    }
    public void doCharge() {
        System.out.println("Electric charging ...");
    }
}

Around 只需要拦截 doCharge()

//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        System.out.println("validating user authority");
        Thread.sleep(1000);
    }

    @Around("execution(* com.spring.puzzle.class6.example1.ElectricService.doCharge()) ")
    public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println("charge method time cost: " + (end - start));
    }
}

原理

Spring 初始化单例类的一般过程,基本都是 getBean()->doGetBean()->getSingleton(),如果发现 Bean 不存在,则调用 createBean()->doCreateBean() 进行实例化。

如果我们的代码里使用了 Spring AOP,doCreateBean() 最终会返回一个代理对象。在代理对象的创建过程中,AbstractAutoProxyCreator#createProxy里面的:**Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);** advisors 就是增强方法对象,它的顺序决定了面临多个增强时,到底先执行谁。而这个集合对象本身是由 specificInterceptors 构建出来的,而 specificInterceptors 又是由 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法构建,它的List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);根据当前的 beanClass、beanName 等信息,结合所有候选的 advisors,最终找出匹配(Eligible)的 Advisor,寻找匹配 Advisor 的逻辑参考 AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    //寻找候选的 Advisor
    List<Advisor> candidateAdvisors = findCandidateAdvisors();
    //根据候选的 Advisor 和当前 bean 算出匹配的 Advisor
    List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    extendAdvisors(eligibleAdvisors);
    if (!eligibleAdvisors.isEmpty()) {
        //排序
        eligibleAdvisors = sortAdvisors(eligibleAdvisors);
    }
    return eligibleAdvisors;
}

最终 Advisors 的顺序是由两点决定:

  1. candidateAdvisors 的顺序;
  2. sortAdvisors 进行的排序。

candidateAdvisors 排序是在 @Aspect 标记的 AopConfig Bean 构建时就决定了。

具体而言,就是在初始化过程中会排序自己配置的 Advisors,并把排序结果存入了缓存(BeanFactoryAspectJAdvisorsBuilder#advisorsCache)。 后续 Bean 创建代理时,直接拿出这个排序好的候选 Advisors。候选 Advisors 排序发生在 Bean 构建这个结论时,可以通过 AopConfig Bean 构建中的堆栈信息验证:

1671893142524-2ddfc5a0-17f3-4a25-94a7-8ae18bd74b50.png

可以看到,排序是在 Bean 的构建中进行的,而最后排序执行的关键代码位于ReflectiveAspectJAdvisorFactory#getAdvisorMethodsmethods.sort(METHOD_COMPARATOR)方法METHOD_COMPARATOR 是定义在 ReflectiveAspectJAdvisorFactory 类中的静态方法块,代码如下:

static {
    //第一个比较器,用来按照增强类型排序
    Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
        // 基准比较器,即在 adviceKindComparator 中最终要调用的比较器,
        // 在构造函数中赋值于 this.comparator;
        new InstanceComparator<>(Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
        // lambda 回调函数,返回传入方法(method)上标记的
        // 将传递的参数(method)转化为基准比较器需要的参数类型:增强注解 Pointcut,Around,Before,After,AfterReturning 以及 AfterThrowing
        // 在构造函数中赋值于 this.converter。
        (Converter<Method, Annotation>) method -> {
            AspectJAnnotation<?> annotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
            return (annotation != null ? annotation.getAnnotation() : null);}
    );
    //第二个比较器,用来按照方法名排序
    Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
    //合并上面两者比较器
    METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}

:::color1
任意两个比较器(Comparator)可以通过 thenComparing() 连接合成一个新的连续比较器;

:::

METHOD_COMPARATOR 本质上是一个连续比较器,由 adviceKindComparatormethodNameComparator 这两个比较器通过 thenComparing() 连接而成。adviceKindComparator 这个比较器,通过实例化 ConvertingComparator 类而来,顾名思义,先转化再比较,查看 ConvertingComparator 比较器核心方法 compare 如下:

public int compare(S o1, S o2) {
    T c1 = this.converter.convert(o1);
    T c2 = this.converter.convert(o2);
    return this.comparator.compare(c1, c2);
}

可知,这里是先调用从构造函数中获取到的 lambda 回调函数 this.converter,经过转化后,我们获取到的待比较的数据其实就是注解了。而它们的排序依赖于 ConvertingComparator 的第一个参数,即最终会调用的基准比较器:

new InstanceComparator<>(
    Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class)

最终我们要调用的基准比较器本质上就是一个 InstanceComparator 类,InstanceComparator 比较器核心方法 compare 如下:

public int compare(T o1, T o2) {
    int i1 = getOrder(o1);
    int i2 = getOrder(o2);
    return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));
}

一个典型的 Comparator,代码逻辑按照 i1、i2 的升序排列,即 InstanceComparator#getOrder返回的值越小,排序越靠前,InstanceComparator#getOrder返回当前传递的增强注解在 this.instanceOrder 中的序列值,序列值越小,则越靠前。

所以 this.instanceOrder 的排序,即为不同类型增强的优先级, 排序越靠前,优先级越高。结论:同一个切面中,不同类型的增强方法被调用的顺序依次为Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class

spring5.3.x版本之前的顺序是上面这样的,之后的顺序是around前置、before、目标方法、afterreturning/afterthrowing、after、around后置

错乱混合同类型增强

:::color4
当同一个切面包含多个同一种类型的多个增强,且修饰的都是同一个方法时,这多个增强的执行顺序是怎样的?

:::

//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.ElectricService.charge())")
    public void logBeforeMethod(JoinPoint pjp) throws Throwable {
        System.out.println("step into ->"+pjp.getSignature());
    }
    @Before("execution(* com.spring.ElectricService.charge()) ")
    public void validateAuthority(JoinPoint pjp) throws Throwable {
        // 鉴权逻辑,鉴权失败,直接报错,不要执行具体的方法
        throw new RuntimeException("authority check failed");
    }
}

step into ->void com.spring.puzzle.class6.example2.Electric.charge()

Exception in thread “main” java.lang.RuntimeException: authority check failed

虽然抛出了异常且 ElectricService.charge() 没有被调用,但是 logBeforeMethod() 的调用日志却被输出了

解决

在同一个切面配置类中,针对同一个方法存在多个同类型增强时,其执行顺序仅和当前增强方法的名称有关,而不是由谁代码在先、谁代码在后来决定。validateAuthority() 改为 checkAuthority(),这种情况下, 对增强(Advisor)的排序,其实最后就是在比较字符 l 和 字符 c。显然易见,checkAuthority()的排序会靠前,从而被优先执行。

//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.ElectricService.charge())")
    public void logBeforeMethod(JoinPoint pjp) throws Throwable {
        System.out.println("step into ->"+pjp.getSignature());
    }
    @Before("execution(* com.spring.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        throw new RuntimeException("authority check failed");
    }
}

原理

METHOD_COMPARATOR 本质是一个连续比较器,第一个比较器 adviceKindComparator,用来按照增强类型排序,第二个比较器 methodNameComparator 用来按照方法名排序,methodNameComparator 最终调用了 String 类自身的 compareTo():如果两个方法名长度相同,则依次比较每一个字母的 ASCII 码,ASCII 码越小,排序越靠前;若长度不同,且短的方法名字符串是长的子集时,短的排序靠前

事件 event

Spring事件是一个相对独立的点。一旦你用上了Spring事件,往往完成的都是一些有趣的、强大的功能,例如动态配置。Spring事件的设计比较简单。说白了,就是监听器设计模式在Spring中的一种实现。Spring事件包含以下三大组件:

  1. 事件(Event):用来区分和定义不同的事件,在Spring中,常见的如ApplicationEvent和AutoConfigurationImportEvent,它们都继承于 java.util.EventObject。
  2. 事件广播器(Multicaster):负责发布上述定义的事件。例如,负责发布ApplicationEvent 的ApplicationEventMulticaster 就是Spring中一种常见的广播器。
  3. 事件监听器(Listener):负责监听和处理广播器发出的事件,例如 ApplicationListener 就是用来处理ApplicationEventMulticaster 发布的 ApplicationEvent,它继承于JDK的 EventListener。

试图处理并不会抛出的事件

:::danger
这段代码定义了一个监听器MyContextStartedEventListener,试图拦截ContextStartedEvent。但是当我们启动Spring Boot后,会发现并不会拦截到这个事件,如何理解这个错误呢?

:::

@Slf4j
@Component
public class MyContextStartedEventListener implements ApplicationListener<ContextStartedEvent> {

    public void onApplicationEvent(final ContextStartedEvent event) {
        log.info("{} received: {}", this.toString(), event);
    }

}

解决

@Component
public class MyContextRefreshedEventListener 
    implements ApplicationListener<ContextRefreshedEvent> {
        
    public void onApplicationEvent(final ContextRefreshedEvent event) {
        log.info("{} received: {}", this.toString(), event);
    }
}

我们监听ContextRefreshedEvent而非ContextStartedEventContextRefreshedEvent的抛出可以参考方法AbstractApplicationContext#finishRefresh

原理

在Spring Boot中,这个事件的抛出只发生在一处,即位于方法AbstractApplicationContext#start中,但是这个方法在Spring Boot启动时会被调用么?我们可以查看Spring启动方法中围绕Context的关键方法调用,代码如下:

public ConfigurableApplicationContext run(String... args) {
    //省略非关键代码
    context = createApplicationContext();
    //省略非关键代码
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    refreshContext(context);
    //省略非关键代码
    return context;
}

我们发现围绕Context、Spring Boot的启动只做了两个关键工作:创建Context和Refresh Context。

很明显,Spring启动最终调用的是AbstractApplicationContext#refresh,并不是 AbstractApplicationContext#start

监听事件的体系不对

@Slf4j
@Component
public class MyApplicationEnvironmentPreparedEventListener
    implements ApplicationListener<ApplicationEnvironmentPreparedEvent > {

    public void onApplicationEvent(final ApplicationEnvironmentPreparedEvent event) {
        log.info("{} received: {}", this.toString(), event);
    }

}

这里我们试图处理ApplicationEnvironmentPreparedEvent。这个事件在Spring中是由EventPublishingRunListener#environmentPrepared方法抛出:

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
    this.initialMulticaster.multicastEvent(
    	new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

:::danger
这个方法在Spring启动时一定经由SpringApplication#prepareEnvironment方法调用,但是监听器的处理并不执行,即拦截不了。

:::

解决

可以把自定义监听器注册到initialMulticaster广播体系中,这里提供两种方法修正问题。

方式一
  1. 在构建Spring Boot时,添加MyApplicationEnvironmentPreparedEventListener:
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        MyApplicationEnvironmentPreparedEventListener myApplicationEnvironmentPreparedEventListener = new MyApplicationEnvironmentPreparedEventListener();
        SpringApplication springApplication = new SpringApplicationBuilder(Application.class).listeners(myApplicationEnvironmentPreparedEventListener).build();
        springApplication.run(args);
    }
}
方式二

使用META-INF/spring.factories,即在/src/main/resources下面新建目录META-INF,然后新建一个对应的spring.factories文件:

org.springframework.context.ApplicationListener=\
com.spring.puzzle.listener.example2.MyApplicationEnvironmentPreparedEventListener

通过上述两种修改方式,即可完成事件的监听,很明显第二种方式要优于第一种,至少完全用原生的方式去解决,而不是手工实例化一个MyApplicationEnvironmentPreparedEventListener。

监听器加载的特殊方式,即使用SPI的方式直接从配置文件META-INF/spring.factories中加载。这种方式或者说思想非常值得你去学习,因为它在许多Java应用框架中都有所使用,例如Dubbo,就是使用增强版的SPI来配置编解码器的。

原理

ApplicationEnvironmentPreparedEvent 的广播器是 EventPublishingRunListenerinitialMulticaster

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    //省略非关键代码
    private final SimpleApplicationEventMulticaster initialMulticaster;

    public EventPublishingRunListener(SpringApplication application, String[] args) {
        //省略非关键代码
        // 广播器
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        for (ApplicationListener<?> listener : application.getListeners()) {
            // 监听器
            this.initialMulticaster.addApplicationListener(listener);
        }
    }
}

ApplicationEnvironmentPreparedEvent的监听器同样位于EventPublishingRunListener中,获取方式参考关键代码行:this.initialMulticaster.addApplicationListener(listener);

如果继续查看代码,我们会发现这个事件的监听器就存储在SpringApplication#Listeners中,调试下就可以找出所有的监听器,从中我们可以发现并不存在我们定义的MyApplicationEnvironmentPreparedEventListener,这是为何?当Spring Boot被构建时,会使用下面的方法去寻找上述监听器:

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

而上述代码最终寻找Listeners的候选者,参考代码 SpringFactoriesLoader#loadSpringFactories中的关键行:

//下面的FACTORIES_RESOURCE_LOCATION定义为 "META-INF/spring.factories"

classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :

我们可以寻找下这样的文件(spring.factories),确实可以发现类似的定义:

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
//省略其他监听器

我们定义的监听器并没有被放置在META-INF/spring.factories中,实际上,我们的监听器监听的体系是另外一套,其关键组件如下:

  1. 广播器:即AbstractApplicationContext#applicationEventMulticaster
  2. 监听器:由上述提及的META-INF/spring.factories中加载的监听器以及扫描到的 ApplicationListener类型的Bean共同组成。

这样比较后,我们可以得出一个结论: 我们定义的监听器并不能监听到initialMulticaster广播出的ApplicationEnvironmentPreparedEvent。

部分事件监听器失效

public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
    }
}

@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> {

    Random random = new Random();

    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info("{} received: {}", this.toString(), event);
        //模拟部分失效
        if(random.nextInt(10) % 2 == 1)
            throw new RuntimeException("exception happen on first listener");
    }
}

@Component
@Order(2)
public class MySecondEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info("{} received: {}", this.toString(), event);
    }
}
@RestController
@Slf4j
public class HelloWorldController {

    @Autowired
    private AbstractApplicationContext applicationContext;

    @RequestMapping(path = "publishEvent", method = RequestMethod.GET)
    public String notifyEvent(){
        log.info("start to publish event");
        applicationContext.publishEvent(new MyEvent(UUID.randomUUID()));
        return "ok";
    };
}

这里监听器MyFirstEventListener的优先级稍高,且执行过程中会有50%的概率抛出异常。观察测试结果,我们会发现监听器MySecondEventListener有一半的概率并没有接收到任何事件。

解决

处理器的执行是顺序执行的,在执行过程中,如果一个监听器执行抛出了异常,则后续监听器就得不到被执行的机会了。

方式一:确保监听器的执行不会抛出异常。
@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        try {
            // 省略事件处理相关代码
        }catch(Throwable throwable){
            //write error/metric to alert
        }

    }
}
方式二:使用org.springframework.util.ErrorHandler

我们设置了一个ErrorHandler,那么就可以用这个ErrorHandler去处理掉异常,从而保证后续事件监听器处理不受影响。

SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = 
    applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, 
                               SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster.
    setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);

其中LOG_AND_SUPPRESS_ERROR_HANDLER的实现如下:

public static final ErrorHandler LOG_AND_SUPPRESS_ERROR_HANDLER = 
    new LoggingErrorHandler();

private static class LoggingErrorHandler implements ErrorHandler {

    private final Log logger = LogFactory.getLog(LoggingErrorHandler.class);

    @Override
    public void handleError(Throwable t) {
        logger.error("Unexpected error occurred in scheduled task", t);
    }
}

原理

具体而言,当广播一个事件,执行的方法参考 SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent)

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

通过Event类型等信息调用 getApplicationListeners 获取了具有执行资格的所有监听器,然后按顺序去执行。最终每个监听器的执行是通过 invokeListener() 来触发的,调用的是接口方法 ApplicationListener#onApplicationEvent最终事件的执行是由同一个线程按顺序来完成的,任何一个报错,都会导致后续的监听器执行不了。

posted on 2025-10-14 23:46  chuchengzhi  阅读(8)  评论(0)    收藏  举报

导航

杭州技术博主,专注分享云计算领域实战经验、技术教程与行业洞察, 打造聚焦云计算技术的垂直博客,助力开发者快速掌握云服务核心能力。

褚成志 云计算 技术博客