龙空技术网

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

JU帮 290

前言:

现时朋友们对“spring整个mybatis中包扫描会创建bean吗”大体比较关怀,朋友们都需要了解一些“spring整个mybatis中包扫描会创建bean吗”的相关知识。那么小编在网摘上汇集了一些关于“spring整个mybatis中包扫描会创建bean吗””的相关资讯,希望我们能喜欢,看官们一起来学习一下吧!

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

案例背景说明

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

第一种:第三方提供 SDK Jar 包,内部封装了 HTTP 发送的方法,这时我们只需要封装相应参数调用方法即可。第二种:第三方提供接口文档,这时就需要我们根据接口文档编写接口、HTTP 客户端发送请求。

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

第三方接口文档

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

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

查询用户信息:UserQueryParamVO

序号

字段名称

字段代码

数据类型

是否必须

备注

1

用户编号

userId

String(32)

Y

2

用户名称

userName

String(100)

Y

查询订单信息:OrderQueryParamVO

序号

字段名称

字段代码

数据类型

是否必须

备注

1

订单编号

orderId

String(32)

Y

2

订单类型

orderType

String(20)

Y

DOMESTIC - 国内

INTERNATIONAL - 国际

项目结构

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

com.jub.model 包

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

com.jub.service 包

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

@ThirdPartyService(handler = CommonThirdPartyServiceHandler.class)public interface ThirdPartyQueryService {    /**     * 查询用户信息     */    void findUser(UserQueryParamVO paramVO);    /**     * 查询订单信息     */    void findOrder(OrderQueryParamVO paramVO);}

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

com.jub.controller 包

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

@RestControllerpublic 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 包:重点、重点、重点

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

ThirdPartyService:用于标识接口为第三方服务接口,本示例中 com.jub.service.ThirdPartyQueryService 使用该注解标注,同时指定用于生成该接口代理类的处理器,即注解的 handler 属性。

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface ThirdPartyService {    /**     * 第三方服务处理器     */    Class<? extends ThirdPartyServiceHandler> handler();}
ThirdPartyServiceScan:用于配置扫描标注 ThirdPartyService 注解的接口的包路径,另外通过 @Import 注解导入一个 ImportBeanDefinitionRegistrar 接口的实现,用于处理扫描接口、注册生成后的代理类。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Import(ThirdPartyServiceRegistrar.class)public @interface ThirdPartyServiceScan {    /**     * 用于配置扫描第三方服务的包路径,如果不配置默认扫描标注的类所在的包和子包     */    String[] value() default {};}

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

ThirdPartyServiceHandler:一个标识接口,其继承了 InvocationHandler 接口,主要作用就是标识第三方服务处理器,方便后面获取,如下所示:

public interface ThirdPartyServiceHandler extends InvocationHandler {    // 该接口仅用于标识第三方服务处理器}
CommonThirdPartyServiceHandler:具体的第三方接口处理器,可以根据需要定义不同的处理器来处理三方接口代理类各个方法的业务,本次以 HTTP 请求为例,如下:
@Slf4jpublic 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 属性指定

ThirdPartyServiceRegistrar:该类实现了 ImportBeanDefinitionRegistrar 和 ResourceLoaderAware 两个接口。ImportBeanDefinitionRegistrar 是用于获取 @Import 注解标注的类的元数据和一个 BeanDefinition 注册器,可以处理 Bean 的注册等其他业务。ResourceLoaderAware 是用于读取资源流信息的接口。通过 ThirdPartyServiceRegistrar 类,可以完成包扫描、接口代理类注册等操作,如下:

@Slf4jpublic 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<MetadataReader> 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<MetadataReader> scanThirdPartyService(AnnotationAttributes attributes,                                                      AnnotationMetadata importingClassMetadata) {        // 2.1 获取扫描的包路径,优先获取 ThirdPartyServiceScan 注解 value 的值,如果数组为空,则获取标注该注解的类的包路径        final List<String> scanPackages =                getScanPackages(attributes, ClassUtils.getPackageName(importingClassMetadata.getClassName()));        log.info("待扫描的包路径为:{}", scanPackages);        final Set<MetadataReader> result = new LinkedHashSet<>();        for (String scanPackage : scanPackages) {            // 2.2 对每个包路径即子包路径下的类进行扫描,获取标注了 ThirdPartyService 数据的接口元信息            result.addAll(scanThirdPartyService(scanPackage));        }        return result;    }    @SneakyThrows    private Set<MetadataReader> scanThirdPartyService(String scanPackage) {        final Set<MetadataReader> 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<String> getScanPackages(AnnotationAttributes attributes, String defaultPackage) {        List<String> 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);    }}
ThirdPartyServiceFactoryBean:用于生成接口代理类
public class ThirdPartyServiceFactoryBean implements FactoryBean<Object> {    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<ThirdPartyServiceHandler> handler = (Class<ThirdPartyServiceHandler>) 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 容器的,有兴趣的小伙伴可以研究一下。

标签: #spring整个mybatis中包扫描会创建bean吗