配置类为什么要添加@Configuration注解?(1)

简介: 配置类为什么要添加@Configuration注解?(1)

不加@Configuration导致的问题


我们先来看看如果不在配置类上添加@Configuration注解会有什么问题,代码示例如下:

@ComponentScan("com.dmz.source.code")
//@Configuration
public class Config{
  @Bean
  public A a(){
  return new A(dmzService());
  }
  @Bean
  public DmzService dmzService(){
    return new DmzService();
  }
}
public class A {
  public A(DmzService dmzService){
    System.out.println("create A by dmzService");
  }
}
@Component
public class DmzService {
  public DmzService(){
    System.out.println("create dmzService");
  }
}
public class Main {
  public static void main(String[] args) {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
  }
}

不添加@Configuration注解运行结果:

create dmzService
create A by dmzService
create dmzService

添加@Configuration注解运行结果:

create dmzService
create A by dmzService

在上面的例子中,我们会发现没有添加@Configuraion注解时dmzService被创建了两次, 这是因为第一次创建是被Spring容器所创建的,Spring调用这个dmzService()创建了一个Bean被放入了单例池中(没有添加其它配置默认是单例的),第二次创建是Spring容器在创建a时调用了a(),而a()又调用了dmzService()方法。


这样的话,就出现问题了。


第一,对于dmzService而言,它被创建了两次,单例被打破了


第二,对于a而言,它所依赖的dmzService不是Spring所管理的,而是直接调用的一个普通的java method创建的普通对象。这个对象不被Spring所管理意味着,首先它的域(Scope)定义失效了,其次它没有经过一个完整的生命周期,那么我们所定义所有的Bean的后置处理器都没有作用到它身上,其中就包括了完成AOP的后置处理器,所以AOP也失效了。

微信图片_20221113154035.png

首先,Spring就在注释中指出了,通常来说,BeanMethod一般都申明在一个被@Configuration注解标注的类中,在这种情况下,BeanMethod可能直接引用了在同一个类中申明的beanMethod,就像本文给出的例子那样,a()直接引用了dmzService(),我们重点再看看划红线的部分,通过调用另外一个beanMethod进入的Bean的引用会被保证是遵从域定义以及AOP语义的,就像getBean所做的那样。这是怎么实现的呢?在最后被红线标注的地方也有说明,是通过在运行时期为没有被@Configuration注解标注的配置类生成一个CGLIB的子类。


源码分析


Spring是在什么时候创建的代理呢?到目前为止我们应该没有落掉Spring整个启动流程的任何关键代码,那么我们不妨带着这个问题继续往下看。目前来说我们已经阅读到了Spring执行流程图中的3-5步,也就是org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors方法。其执行逻辑如下:

微信图片_20221113154108.jpg

在之前的分析中我们已经知道了,这个方法的主要作用就是执行BeanFactoryPostProcessor中的方法,首先执行的是BeanDefinitionRegistryPostProcessor(继承了BeanFactoryPostProcessor)的postProcessBeanDefinitionRegistry方法,然后执行postProcessBeanFactory方法。那么目前为止容器中有哪些BeanFactoryPostProcessor呢?Spring内置的BeanFactoryPostProcessor并且在当前这个时机就已经被注册到容器中的只有一个,就是ConfigurationClassPostProcessor

,在之前的文章中我们已经分析过了它的postProcessBeanDefinitionRegistry方法,这个方法主要是为了完成配置类的解析以及对组件的扫描,紧接着我们就来看看它的postProcessBeanFactory方法做了什么。其源码如下:

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
   int factoryId = System.identityHashCode(beanFactory);
   // 防止重复处理
   if (this.factoriesPostProcessed.contains(factoryId)) {
       throw new IllegalStateException(
           "postProcessBeanFactory already called on this post-processor against " + beanFactory);
   }
   this.factoriesPostProcessed.add(factoryId);
   // 在执行postProcessBeanDefinitionRegistry方法的时就已经将这个id添加到registriesPostProcessed集合中了
   if (!this.registriesPostProcessed.contains(factoryId)) {
       processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
   }
  // 看起来这个方法就是完成了代理
   enhanceConfigurationClasses(beanFactory);
   // 添加了一个后置处理器
   beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

enhanceConfigurationClasses源码分析

  public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {    // map中放置的是所有需要被代理的类
    Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
    for (String beanName : beanFactory.getBeanDefinitionNames()) {
      BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
      if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {
        // 省略异常跟日志代码....
               // 这个代码的含义就是如果是一个被@Configuration注解标注的类,那么将其放入到configBeanDefs这个集合中
        configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
      }
    }
    if (configBeanDefs.isEmpty()) {
      // nothing to enhance -> return immediately
      return;
    }
       // 对配置类进行代理的核心类
    ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
    for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
      AbstractBeanDefinition beanDef = entry.getValue();
      // 对于配置类永远使用cglib代理
      beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
      try {
        // cglib代理是基于类实现的,所以在这之前要明确代理的类是什么
        Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);
        if (configClass != null) {
                   // 通过ConfigurationClassEnhancer获取到一个经过代理的class
          Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
                  // 省略日志....
                   // 将原有的配置类的bd中的beanClass属性替换成代理后的class
            beanDef.setBeanClass(enhancedClass);
          }
        }
      }
      catch (Throwable ex) {
        throw new IllegalStateException("Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
      }
    }
  }

这段代码非常简单,其目的就是生成一个enhancedClass(经过了cglib增强的class),然后用其替换目标配置类对应的BeanDefinition中的beanClass属性。那么我们接下来需要分析的就是enhancedClass是如何生成的?其核心的代码在ConfigurationClassEnhancer中,所以我们要分析下ConfigurationClassEnhancer的源码,在分析它的源码前,我们需要对cglib有一定的了解。


1、cglib原理分析


1.1、使用示例


public class Target{
   public void f(){
       System.out.println("Target f()");
   }
   public void g(){
       System.out.println("Target g()");
   }
}
public class Interceptor implements MethodInterceptor {
   @Override
   public Object intercept(Object obj, Method method, Object[] args,    MethodProxy proxy) throws Throwable {
       System.out.println("I am intercept begin");
//Note: 此处一定要使用proxy的invokeSuper方法来调用目标类的方法
       proxy.invokeSuper(obj, args);
       System.out.println("I am intercept end");
       return null;
   }
}
public class Test {
   public static void main(String[] args) {
       // 设置这个属性,将代理类的字节码文件生成到F盘的code目录下
   System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\code");
        //实例化一个增强器,也就是cglib中的一个class generator
       Enhancer eh = new Enhancer();
        //设置目标类
       eh.setSuperclass(Target.class);
       // 设置拦截对象
       eh.setCallback(new Interceptor());
       // 生成代理类并返回一个实例
       Target t = (Target) eh.create();
       t.f();
       t.g();
   }
}

运行结果为:

I am intercept begin
Target f()
I am intercept end
I am intercept begin
Target g()
I am intercept end

可以看到,通过上面这种方式,我们已经对Target中的方法完成了增强(可以在方法执行前后插入我们自己的定制的逻辑)


1.2、原理分析


查看F盘的code目录,会发现多了以下几个文件

微信图片_20221113154434.png

其中第二个文件就是我们的代理类字节码,将其直接用IDEA打开

// 省略多余的方法,我们就关注g方法
public class Target$$EnhancerByCGLIB$$788444a0 extends Target implements Factory
{
   final void CGLIB$g$0()
   {
     super.g();
   }
   // 经过代理过的g方法
   public final void g()
   {
   // 查看是否有拦截器存在
     MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
     if (tmp4_1 == null)
     {
         CGLIB$BIND_CALLBACKS(this);
         tmp4_1 = this.CGLIB$CALLBACK_0;
     }
     // 如果有拦截器的存在的话,直接调用拦截器的方法
     if (this.CGLIB$CALLBACK_0 != null) {
         tmp4_1.intercept(this, CGLIB$g$0$Method, CGLIB$emptyArgs, CGLIB$g$0$Proxy);
     }
     // 如果没有拦截器,说明不需要代理,直接调用父类方法,也就是目标类的方法
     else{
         super.g();
     }
   }
}
从上面的代码中我们可以看出
1. 代理类继承了目标类
2. 目标类中的方法在代理类中对应了两个方法,就以上面例子中的目标类中的g()方法为例,其对应的代理类中的两个方法为
- CGLIB$g$0()
- g()

它们之间的关系如下所示

微信图片_20221113154600.png

实际被增强的方法是代理对象中的g方法,当它被调用时,在执行的过程中会调用CGLIB$g$0()方法,而这个方法又会调用代理类的父类,也就是目标类中的g()。

从这里就能看出,跟JDK动态代理不同的是,cglib代理采用的是继承的方式生成的代理对象。


在上面的例子中,我们实现了对cglib中方法的拦截,但是就目前而言我们没有办法选择性的拦截目标类中的某一个方法,假设现在我们只想拦截Target中的g方法而不拦截f方法有什么方法呢?我们看下面这个例子

public class Main {
  public static void main(String[] args) {
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\code");
    //实例化一个增强器,也就是cglib中的一个class generator
    Enhancer eh = new Enhancer();
    //设置目标类
    eh.setSuperclass(Target.class);
    // 设置拦截对象
    eh.setCallbacks(new Callback[]{new Interceptor(), NoOp.INSTANCE});
    eh.setCallbackFilter(new CallbackFilter() {
      @Override
      public int accept(Method method) {
        if(method.getName().equals("g"))
             // 这里返回的是上面定义的callback数组的下标,0就是我们的Interceptor对象,1是内置的NoOp对象,代表不做任何操作
        return 0;
        else return 1;
      }
    });
    // 生成代理类并返回一个实例
    Target t = (Target) eh.create();
    t.f();
    t.g();
  }
}

运行结果:

Target f()
I am intercept begin
Target g()
I am intercept end

此时f方法已经不会被代理了
/

相关文章
|
24天前
|
XML Java 数据格式
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
本文介绍了如何使用Spring框架的注解方式实现AOP(面向切面编程)。当目标对象没有实现接口时,Spring会自动采用CGLIB库进行动态代理。文中详细解释了常用的AOP注解,如`@Aspect`、`@Pointcut`、`@Before`等,并提供了完整的示例代码,包括业务逻辑类`User`、配置类`SpringConfiguration`、切面类`LoggingAspect`以及测试类`TestAnnotationConfig`。通过这些示例,展示了如何在方法执行前后添加日志记录等切面逻辑。
59 2
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
|
1月前
|
Java Spring 容器
Springboot3.2.1搞定了类Service和bean注解同名同类型问题修复
这篇文章讨论了在Spring Boot 3.2.1版本中,同名同类型的bean和@Service注解类之间冲突的问题得到了解决,之前版本中同名bean会相互覆盖,但不会在启动时报错,而在配置文件中设置`spring.main.allow-bean-definition-overriding=true`可以解决这个问题。
68 0
Springboot3.2.1搞定了类Service和bean注解同名同类型问题修复
|
4月前
|
XML 存储 Java
@Configuration 注解使用及源码解析
@Configuration 注解使用及源码解析
32 4
|
6月前
|
XML Java 数据格式
@Configuration配置类注解的理解
@Configuration配置类注解的理解
@Configuration配置类注解的理解
|
6月前
|
Java 数据库连接 API
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
662 0
|
XML 前端开发 Java
Spring-基于注解的配置[01定义Bean+扫描Bean]
Spring-基于注解的配置[01定义Bean+扫描Bean]
121 0
|
Java 容器 Spring
Spring基础篇:利用注解将外部Properties属性注入到Bean中的方法
利用注解将外部Properties属性注入到Bean中的方法
164 0
|
XML Java 数据格式
【Spring注解必知必会】深度解析@Configuration注解
【Spring注解必知必会】深度解析@Configuration注解
267 0
【Spring注解必知必会】深度解析@Configuration注解
|
缓存 Java Spring
配置类为什么要添加@Configuration注解?(2)
配置类为什么要添加@Configuration注解?(2)
177 0
配置类为什么要添加@Configuration注解?(2)
|
Java Spring 容器
@Configuration注解
@Configuration注解
159 0