在《你真正了解 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 容器中,在下面解释。
首先我们先介绍两个注解:
@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
调用第三方的接口业务逻辑基本一致,都是获取到参数发送 HTTP 请求,通过上面的方式可以减少很多相同的代码,方便维护。
处理上面的使用场景,在 MyBatis 中也是使用这种方式生成 Mapper 实现类,并注入到 IOC 容器的,有兴趣的小伙伴可以研究一下。
页面更新:2024-04-22
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号