SpringBoot 如何实现自定义扫描和注册类?

在《你真正了解 Spring @Import 注解吗?》文章中,我们学习了 Spring @Import 注解和使用。在本文中,我们将结合一个实际开发的案例,学习在实际开发中如何使用 @Import 注解导入 ImportBeanDefinitionRegistrar 接口的实现类。

案例背景说明

在项目中,通常会调用第三方服务商提供的服务,例如:短信、支付等。调用第三方服务通常也是采用 HTTP 的方式,这时就会有两种调用情况:

本次案例模拟第二种方式调用三方的服务。

第三方接口文档

第三方接口文档,通常会定义各个接口请求和响应的字段的数据类型、长度、描述、枚举值等,对于存在文件上传下载、加解密、加签验签等场景时,会提供调用说明。

本部分仅仅提供案例测试使用的请求接口文档,如下所示:

序号

字段名称

字段代码

数据类型

是否必须

备注

1

用户编号

userId

String(32)

Y


2

用户名称

userName

String(100)

Y


序号

字段名称

字段代码

数据类型

是否必须

备注

1

订单编号

orderId

String(32)

Y


2

订单类型

orderType

String(20)

Y

DOMESTIC - 国内

INTERNATIONAL - 国际

项目结构

本次示例类较多,如上图所示,下面我们分别解释各个类的作用。

UserQueryParamVO、OrderQueryParamVO 分别对应第三方接口文档的用户和订单信息,给第三方发送 HTTP 请求,就使用这两个 Bean。字段同上面表格,在此就不展示代码了。

ThirdPartyQueryService 定义了第三方要求的接口,本示例为查询用户信息和订单信息,如下所示:

@ThirdPartyService(handler = CommonThirdPartyServiceHandler.class)
public interface ThirdPartyQueryService {

    /**
     * 查询用户信息
     */
    void findUser(UserQueryParamVO paramVO);

    /**
     * 查询订单信息
     */
    void findOrder(OrderQueryParamVO paramVO);
}

接口的标注的 @ThirdPartyService 注解稍后解释

用于测试上面第三方接口调用,如下所示:

@RestController
public class InvokeThirdPartyController {

    @Autowired
    private ThirdPartyQueryService thirdPartyQueryService;

    @GetMapping("/invoke/third/party/{type}")
    public void invoke(@PathVariable String type) {
        switch (type) {
            case "user":
                UserQueryParamVO userParam = new UserQueryParamVO().setUserId("1001").setUserName("张三");
                thirdPartyQueryService.findUser(userParam);
                break;
            case "order":
                OrderQueryParamVO orderParam = new OrderQueryParamVO().setOrderId("TD_10010_0001")
                        .setOrderType(OrderQueryParamVO.OrderType.DOMESTIC);
                thirdPartyQueryService.findOrder(orderParam);
                break;
            default:
                throw new IllegalArgumentException("不支持类型:" + type);
        }
    }
}

在该 Controller 中主动注入 ThirdPartyQueryService 的实现类,因为代码我们就定义了接口,具体实现类如何生成,又如何注入到 IOC 容器中,在下面解释。

com.jub.proxy 包:重点、重点、重点

首先我们先介绍两个注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThirdPartyService {

    /**
     * 第三方服务处理器
     */
    Class<? extends ThirdPartyServiceHandler> handler();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ThirdPartyServiceRegistrar.class)
public @interface ThirdPartyServiceScan {

    /**
     * 用于配置扫描第三方服务的包路径,如果不配置默认扫描标注的类所在的包和子包
     */
    String[] value() default {};
}

下面是三个关键类和一个标识接口,具体如下:

public interface ThirdPartyServiceHandler extends InvocationHandler {
    // 该接口仅用于标识第三方服务处理器
}
@Slf4j
public class CommonThirdPartyServiceHandler implements ThirdPartyServiceHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在此可以编写给第三方发送 HTTP 请求的业务逻辑
        log.info("===> 发送请求到第三方了:{}", proxy.getClass().getName());
        return null;
    }
}

具体哪个第三方接口采用哪个处理器,通过 ThirdPartyService 注解 handler 属性指定

@Slf4j
public class ThirdPartyServiceRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    private ResourcePatternResolver resourcePatternResolver;

    private CachingMetadataReaderFactory metadataReaderFactory;

    private static final String PACKAGE_SEPARATOR = ".";

    private static final String PATH_SEPARATOR = "/";

    private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 1、获取 ThirdPartyServiceScan 注解属性 Map
        AnnotationAttributes attributes =
                AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(ThirdPartyServiceScan.class.getName()));
        if (attributes == null) {
            return;
        }

        log.info("================= 扫描标注 ThirdPartyService 注解的接口开始 =================");
        // 2、扫描并获取标注 ThirdPartyService 注解接口元信息
        Set thirdPartyServices = scanThirdPartyService(attributes, importingClassMetadata);
        for (MetadataReader thirdPartyService : thirdPartyServices) {
            // 3、将每个接口动态代理生成代理类,并将代理类注册到 IOC 容器中
            registerThirdPartyService(thirdPartyService, registry);
        }
        log.info("================= 扫描标注 ThirdPartyService 注解的接口结束 =================");
    }

    /**
     * 将扫描到的接口通过 FactoryBean 生成其实现类,并将实现类注入到 IOC 容器中
     *
     * @param thirdPartyService 接口元数据
     * @param registry          BeanDefinitionRegistry
     */
    private void registerThirdPartyService(MetadataReader thirdPartyService, BeanDefinitionRegistry registry) {
        String beanName = thirdPartyService.getClassMetadata().getClassName();
        // 3.1 通过 BeanDefinitionBuilder 构建一个 BeanDefinition,genericBeanDefinition 需要的入参为 Bean 的名称,
        // 此处使用接口名称
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanName);
        // 3.2 此处可以为构建 Bean 实例传递必要的参数,此处通过构造器传参,使用 addConstructorArgValue 方法,如果通过 Setter 
        // 方法传参,则使用 addPropertyValue 方法。
        builder.addConstructorArgValue(thirdPartyService);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        // 3.3 此处需要注意,如果添加的是普通的类,那么就直接创建该类的实例,但是如果传入的是 FactoryBean 接口的实例,那么就会
        // 调用 getObject 方法获取需要注入的实例对象。
        beanDefinition.setBeanClass(ThirdPartyServiceFactoryBean.class);
        // 3.4 将 BeanDefinition 添加到注册器中,Spring 会更加 BeanDefinition 构建 Bean 的实例,并添加到 IOC 容器中
        registry.registerBeanDefinition(beanName, beanDefinition);
    }

    /**
     * 扫描 {@link ThirdPartyService} 注解标注的接口
     *
     * @param attributes             {@link ThirdPartyServiceScan#value()}
     * @param importingClassMetadata {@link ThirdPartyServiceScan} 注解标注类的元信息
     * @return 接口类别
     */
    private Set scanThirdPartyService(AnnotationAttributes attributes,
                                                      AnnotationMetadata importingClassMetadata) {
        // 2.1 获取扫描的包路径,优先获取 ThirdPartyServiceScan 注解 value 的值,如果数组为空,则获取标注该注解的类的包路径
        final List scanPackages =
                getScanPackages(attributes, ClassUtils.getPackageName(importingClassMetadata.getClassName()));
        log.info("待扫描的包路径为:{}", scanPackages);
        final Set result = new LinkedHashSet<>();
        for (String scanPackage : scanPackages) {
            // 2.2 对每个包路径即子包路径下的类进行扫描,获取标注了 ThirdPartyService 数据的接口元信息
            result.addAll(scanThirdPartyService(scanPackage));
        }
        return result;
    }

    @SneakyThrows
    private Set scanThirdPartyService(String scanPackage) {
        final Set result = new LinkedHashSet<>();
        // 2.2.1 将包路径转换为:classpath*:com/jub/xxx/**/*.class,如果只需要配当前包下的类,那么吧 ** 去掉即可
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                convertPackageToResourcePath(scanPackage) + '/' + DEFAULT_RESOURCE_PATTERN;
        // 2.2.2 获取包路径对应的资源信息,也就是对应的类文件信息
        Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
        for (Resource resource : resources) {
            // 2.2.3 将类文件流转换为类的元数据,方便获取类信息,如类的元数据、标注注解的元数据
            MetadataReader reader = metadataReaderFactory.getMetadataReader(resource);
            AnnotationMetadata annotationMetadata = reader.getAnnotationMetadata();
            ClassMetadata classMetadata = reader.getClassMetadata();
            // 2.2.4 判断当前类为接口,并且标注了注解 ThirdPartyService,添加到结果集中
            if (classMetadata.isInterface() && annotationMetadata.isAnnotated(ThirdPartyService.class.getName())) {
                log.info("扫描到接口:{}", classMetadata.getClassName());
                result.add(reader);
            }
        }
        return result;
    }

    /**
     * 获取扫描的包路径
     *
     * @param attributes     {@link ThirdPartyServiceScan#value()}
     * @param defaultPackage 默认扫描的包路径,即 {@link ThirdPartyServiceScan} 注解标注类的所在包
     * @return 待扫描包路径集合
     */
    private List getScanPackages(AnnotationAttributes attributes, String defaultPackage) {
        List scanPackages = Arrays.stream(attributes.getStringArray("value"))
                .filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
        if (CollUtil.isEmpty(scanPackages)) {
            scanPackages.add(defaultPackage);
        }
        return scanPackages;
    }

    private static String convertPackageToResourcePath(String packageName) {
        return packageName.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR);
    }
}
public class ThirdPartyServiceFactoryBean implements FactoryBean {

    private final MetadataReader thirdPartyService;

    public ThirdPartyServiceFactoryBean(MetadataReader thirdPartyService) {
        this.thirdPartyService = thirdPartyService;
    }

    @Override
    public Object getObject() throws Exception {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(thirdPartyService.getAnnotationMetadata()
                .getAnnotationAttributes(ThirdPartyService.class.getName()));
        if (attributes == null) {
            throw new IllegalArgumentException("未找到 ThirdPartyService 注解");
        }
        Class handler = (Class) attributes.getClass("handler");
        Class<?> interfaceClass = Class.forName(thirdPartyService.getClassMetadata().getClassName());
        return Proxy.newProxyInstance(ThirdPartyServiceFactoryBean.class.getClassLoader(), new Class[]{interfaceClass},
                handler.getDeclaredConstructor().newInstance());
    }

    @SneakyThrows
    @Override
    public Class<?> getObjectType() {
        return Class.forName(thirdPartyService.getClassMetadata().getClassName());
    }
}

总结

调用第三方的接口业务逻辑基本一致,都是获取到参数发送 HTTP 请求,通过上面的方式可以减少很多相同的代码,方便维护。

处理上面的使用场景,在 MyBatis 中也是使用这种方式生成 Mapper 实现类,并注入到 IOC 容器的,有兴趣的小伙伴可以研究一下。



页面更新:2024-04-22

标签:注解   字段   容器   路径   处理器   接口   订单   文档   方法   信息

1 2 3 4 5

上滑加载更多 ↓
Top