Spring-autoconfigure 中@ConditionalOnClass注解是如何判断是否存在class Demo式例

468 阅读4分钟

在阅读spring-boot源码中 关于自动装配这一块的信息我们经常会在 XXXAutoConfiguration类中发现一些class缺失没有如:RedisAutoConfiguration举例

Snipaste_2022-06-21_09-47-56.png

这样的缺少class如何成功打包 是如何成功编译的?

我们可以在github中找到spring-boot项目中的autoconfigure项目

在这个文件中 spring-boot/spring-boot-project/spring-boot-autoconfigure/build.gradle

由于springboot采用的gradle 进行管理大致功能与 maven类似

optional("org.springframework.data:spring-data-jdbc")
   optional("org.springframework.data:spring-data-ldap")
   optional("org.springframework.data:spring-data-mongodb")
   optional("org.springframework.data:spring-data-neo4j")
   optional("org.springframework.data:spring-data-r2dbc")
   optional("org.springframework.data:spring-data-redis")
   optional("org.springframework.hateoas:spring-hateoas")
   optional("org.springframework.security:spring-security-acl")
   optional("org.springframework.security:spring-security-config")
   optional("org.springframework.security:spring-security-data") {
      exclude group: "javax.xml.bind", module: "jaxb-api"
   }
   optional("org.springframework.security:spring-security-messaging")
   optional("org.springframework.security:spring-security-oauth2-client")
   optional("org.springframework.security:spring-security-oauth2-jose")
   optional("org.springframework.security:spring-security-oauth2-resource-server")
   optional("org.springframework.security:spring-security-rsocket")
   optional("org.springframework.security:spring-security-saml2-service-provider")
   optional("org.springframework.security:spring-security-web")

我们可以看到这个项目引入很多依赖但我我们在引用boot的时候为什么没有自动引入这些的?

原因就在于optional可选依赖

maven的话就是

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>

当你使用这个时依赖就不会显式的进行引用

至于为什么可以成功编译 打jar我就不用多说了吧 (人家项目已经依赖的已经)

那么的话又会引发一个问题当我们new这个RedisAutoConfiguration对象时会不会抱错呢?

RedisAutoConfiguration redisAutoConfiguration = new RedisAutoConfiguration();

可以正常实例化

当我们再调用实例化后的redisTemplate的这个方法呢?

RedisAutoConfiguration redisAutoConfiguration = new RedisAutoConfiguration();
redisAutoConfiguration.redisTemplate(null);

哦 NO编译失败了
/Users/ONEX/programming/code/test-code/src/main/java/com/onex/annon/AnnTest.java:25:45
java: 无法访问org.springframework.data.redis.core.RedisTemplate
  找不到org.springframework.data.redis.core.RedisTemplate的类文件
      

为什么可以正常实例化但是调用此类的方法时就会引发起编译失败呢?

实例化后在Java内存中RedisAutoConfiguration.class到底长什么样子?

我们可以通过 jdk8中的/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进行查看

反编译的class.png

你会发现有些import,注解没有了 具体为什么会没有我也不知道🤷‍♂️反正通过

RedisAutoConfiguration.class 获取类上面的注解是可以正常获取到的

那么就回到上面那个问题(为什么可以正常实例化但是调用此类的方法时就会引发起编译失败呢?)

Java在编译期会将你使用到的类进行编译

可以写一个 Demo

package com.onex.demo;

/**
 * @author buhuazhen
 * @date 2022/6/21
 * @email 3038525872@qq.com
 */
public class StaticDemo {
    static {
        System.out.println("StaticDemo static");
    }
}




package com.onex.demo;

import com.onex.demo.StaticDemo;

/**
 * @author buhuazhen
 * @date 2022/6/21
 * @email 3038525872@qq.com
 */
public class Main {
    public static void main(String[] args) {
        System.out.println("gogogo");
    }
}

结果:
      gogogo

static {} 运行在类的首次加载中

但是启动Main类时可以清楚的看出来没有去加载 StaticDemo 所以StaticDemo将不会去校验是否会存在

清楚了这点之后我们对于Spring-autoconfigure 显示class找不到却可以正常启动就有了一个理解了

那么回到文章的问题 @ConditionalOnClass(RedisOperations.class) spring是如何进行校验class是否存在的呢

有的朋友将会想了 这不简单 通过 .class 获取注解信息不接可以了吗

RedisAutoConfiguration redisAutoConfiguration = new RedisAutoConfiguration();
        ConditionalOnClass annotation = redisAutoConfiguration.getClass().getAnnotation(ConditionalOnClass.class);
        Class<?>[] value = annotation.value();
        for (Class< ? > v : value) {
            System.out.println("存不存在呢\t"+v);
        }

结果:

Exception in thread "main" java.lang.TypeNotPresentException: Type org.springframework.data.redis.core.RedisOperations not present
Caused by: java.lang.ClassNotFoundException: org.springframework.data.redis.core.RedisOperations

报class找不到....

当然了 校验一个class对象是否存在 class对象 这不很自相矛盾吗

:那么通过判断会不会报错不就可以判断出来了 class会不会存在吗

你这个小机灵鬼 可以是可以的但spring 没有这么做

spring源码中没有这么做可能是为了 与name()公用同一套的逻辑吧

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {

   /**
    * The classes that must be present. Since this annotation is parsed by loading class
    * bytecode, it is safe to specify classes here that may ultimately not be on the
    * classpath, only if this annotation is directly on the affected component and
    * <b>not</b> if this annotation is used as a composed, meta-annotation. In order to
    * use this annotation as a meta-annotation, only use the {@link #name} attribute.
    * @return the classes that must be present
    */
   Class<?>[] value() default {};

   /**
    * The classes names that must be present.
    * @return the class names that must be present.
    */
   String[] name() default {};

}

那么我们如何获取这个不存在的 class对象呢?

很抱歉通过Java自带的一些属性无法获取到

只能通过 ASM Java字节码技术进行获取 关于ASM大家可以自己去了解我这里不过多介绍

Spring对ASM自己实现了 提供了一个类

org.springframework.core.type.classreading.SimpleMetadataReaderFactory 这个类

废话不多说直接上代码

package com.onex.demo;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;

import java.util.Map;

/**
 * @author buhuazhen
 * @date 2022/6/21
 * @email 3038525872@qq.com
 */
public class Main {
    public static void main(String[] args) throws Exception {
        RedisAutoConfiguration redisAutoConfiguration = new RedisAutoConfiguration();
        SimpleMetadataReaderFactory simpleMetadataReaderFactory = new SimpleMetadataReaderFactory(Thread.currentThread().getContextClassLoader());
        MetadataReader metadataReader = simpleMetadataReaderFactory.getMetadataReader(RedisAutoConfiguration.class.getName());
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        // 可以获取到 ConditionalOnClass 以方法名:值 的map集合
        Map<String, Object> annotationAttributes = annotationMetadata.getAnnotationAttributes(ConditionalOnClass.class.getName(), true);
        // 怎么能获取到一个不存在class的class对象中 所以这里返回的是class的全限定类名
        String[] classNames = (String[]) annotationAttributes.get("value");
        // 打印出来
        for (String className : classNames) {
            System.out.println(className);
        }
        // 有了 全限定类名再判断有没有class 不接很简单了名
        for (String className : classNames) {
            try{
                Class.forName(className);
                System.out.println("存在"+className);
            }catch (Exception e){
                System.out.println("不存在"+className);
            }
        }
    }
}

Spring 的源码中也是通 Class.forName(className) 进行判断的哦

org.springframework.boot.autoconfigure.condition.FilteringSpringBootCondition

static boolean isPresent(String className, ClassLoader classLoader) {
         if (classLoader == null) {
            classLoader = ClassUtils.getDefaultClassLoader();
         }
         try {
            resolve(className, classLoader);
            return true;
         }
         catch (Throwable ex) {
            return false;
         }
      }

至此spring 是如何判断class是否存在的一个简单Demo也就完成了