在平时我们使用Spring的过程中,往往会使用不同的配置来区分不同环境下Spring bean的加载。尤其是不同的系统集成在一个工程中时,也就是说代码上是一个大的工程,但是部署时是分开的,这时不同的系统间存在代码复用的可能性就很大,而且很多时候一个接口会有多个实现bean,发生冲突的可能性也不小,那如何来灵活控制Spring bean的加载呢?
一般有以下几种方式来控制Spring bean的加载:
1.使用Spring的profile来控制;
2.自定义标注来控制bean的加载。
很多人都知道怎么通过Spring的profile来控制是否加载某个bean,一般都是以下的方式:
@Profile("admin")
@Service
public class TestServiceImpl implements TestService {
}
然后在application.properties中我们会配置:
spring.profiles.include=admin
这样这个bean就会被加载。
那使用profile就能满足我们的一切场景了吗?no,如果全部通过profile来控制,有以下缺点:
1.当这样的bean很多时,你会苦恼于配哪些profile,会有一种无所适从的感觉,而且这种方式的配置往往跟业务无关,一段时间后你会发现你配置的profile自己都不知道是什么意思;
2.profile只能根据上述配置控制这个bean加载或者不加载,在某些场合我们甚至需要SPEL表达式来灵活控制bean的加载,它做不到。
所以我们需要自定义标注,以实现更加灵活的控制,例如:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(RegisterIfCondition.class) //控制bean加载的具体逻辑
public @interface RegisterIf {
String value();
}
public class RegisterIfCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(RegisterIf.class.getName()));
BeanFactoryContextExpressionResolver resolver = new BeanFactoryContextExpressionResolver();
resolver.setBeanFactory(context.getBeanFactory());
resolver.setEnvironment(context.getEnvironment());
return resolver.resolveAsBoolean(attributes.getString("value"));
}
}
这样你就可以以下面的方式来控制这个bean是否加载:
@RegisterIf("${TestServiceImpl.enabled:false}")
@Service
public class TestServiceImpl implements TestService {
}
然后在application.properties中加以下配置就能让它加载:
TestServiceImpl.enabled=true #这样的配置就更加清晰,一看就知道是跟哪个业务相关的
笔者在实现后端服务化的过程中,在开发环境,我们都希望能跑在单体应用中来调试各种问题,单体应用跟服务化后的环境是完全不同的,服务化后一些接口间的调用往往都是RPC调用,然而单体应用调试时并不需要RPC调用。也就是说,接口有服务端版本和客户端版本两个实现bean,运行在本地开发模式时加载服务端版本,运行在服务化模式时加载客户端版本。代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(ApiImportCondition.class)
public @interface ApiImport { //服务端版本
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(ApiImportCondition.class)
public @interface ApiClientImport { //客户端版本
String value();
}
public class ApiImportCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String service, mode;
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(ApiClientImport.class.getName()));
if (CollectionUtils.isEmpty(attributes)) {
attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(ApiImport.class.getName()));
mode = "server";
} else {
mode = "client";
}
if (CollectionUtils.isEmpty(attributes)) {
throw new RuntimeException("Invalid annotation");
}
service = attributes.getString("value");
if (!StringUtils.hasLength(service)) {
throw new RuntimeException("service name need specified");
}
BeanFactoryContextExpressionResolver resolver = new BeanFactoryContextExpressionResolver();
resolver.setBeanFactory(context.getBeanFactory());
resolver.setEnvironment(context.getEnvironment());
String expr = String.format("#{'${%1s.run.mode:server}'.equalsIgnoreCase('%2s')}", service, mode); //默认运行在服务端模式,只有在运行在客户端模式时才需要此配置
return resolver.resolveAsBoolean(expr);
}
}
我们可以这么使用:
@ApiImport("test-service")
@Service
public class TestServiceImpl implements TestService {
//服务端版本
}
@ApiClientImport("test-service")
@Service
public class TestApiServiceImpl implements TestService {
//客户端版本
//RPC调用
}
然后我们就可以在application.properties中控制是加载服务端的bean还是客户端的bean:
test-service.run.mode=client #该test service将运行在服务化的模式
希望这篇文章对你有用,也希望能提出宝贵的意见,谢谢!
文章已收录在此: