springcloud系列学习笔记目录参见博主专栏 spring boot 2.X/spring cloud Greenwich。
书接上文。
前面讲了springboot的实现基础是spring的@Conditional注解。介绍原理前我们来看看怎么用。后面介绍其原理。
我们实现这么一个小功能:根据不同的环境,实例化不同的bean。
springboot通常都是通过-Dspring.profiles.active=dev
来区分环境的,如果我们想实现线上的代码逻辑与开发或者测试环境不同,那么这是一个解决方案。
使用java的多态,先定义一个接口:
public interface EnvironmentService {
void printEnvironment();
}
然后定义两个不同环境的实现类,内容比较简单,只是输出一句log。
@Slf4j
public class ProdService implements EnvironmentService {
@Override
public void printEnvironment() {
log.info("我是生产环境。");
}
}
@Slf4j
public class DevService implements EnvironmentService{
@Override
public void printEnvironment() {
log.info("我是开发环境。");
}
}
好了,,准备工作已经做完,接下来编写配置类,使用spring的@Conditional注解来根据不同的环境配置加载不同的类:
import com.example.demo.environment.impl.DevService;
import com.example.demo.environment.impl.ProdService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 根据不同的环境,初始化不同的bean
*/
@Configuration
@Slf4j
public class MySpringBootConfig {
@ConditionalOnExpression("#{'${spring.profiles.active}'.equals('dev')}")//使用了spring的SPEL表达式
@ConditionalOnClass(DevService.class)
@Bean
public DevService initDevService() {
log.info("DevService已加载。");
return new DevService();
}
@ConditionalOnExpression("#{'${spring.profiles.active}'.equals('prod')}")
@ConditionalOnClass(ProdService.class)
@Bean
public ProdService initProdService() {
log.info("ProdService已加载。");
return new ProdService();
}
}
condition有很多:
这里我们使用的是@ConditionalOnExpression
。它根据SPEL表达式返回的结果作为条件判断。
这里判断条件为:spring.profiles.active=dev时,创建DevService。为prod时,创建ProdService。
接下来看如何使用:
import com.example.demo.environment.EnvironmentService;
import com.example.myservice.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@SpringBootApplication
public class DemoApplication {
@Autowired
HelloService helloService;
@Autowired
EnvironmentService environmentService;
@Value("#{'${spring.profiles.active}'.equals('dev')}")
String springProfilesActive;
@RequestMapping("/")
public String word(String name) {
environmentService.printEnvironment();
log.info("springProfilesActive={}", springProfilesActive);
return helloService.hello(name);
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
很简单,只需要注入EnvironmentService即可。注意IDEA可能会提示错误,因为这个接口有两个实现类。不过不用去管他,因为我们通过condition只实例化了一个bean。
运行结果:
我是开发环境。
springProfilesActive=true
initVal=hahaha, name=hhh
将配置修改为prod后:
我是生产环境。
springProfilesActive=true
initVal=hahaha, name=hhh
使用非常简单,那么@Conditional是怎么实现的呢?
官方文档的说明是**“只有当所有指定的条件都满足是,组件才可以注册”**。主要的用处是在创建bean时增加一系列限制条件。
类图:
核心类:
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
所有的condition都是Condition接口的实现类,条件判断是通过matches方法返回的布尔值来判断的。
以@ConditionalOnExpression
注解为例:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnExpressionCondition.class)
public @interface ConditionalOnExpression {
/**
* The SpEL expression to evaluate. Expression should return {@code true} if the
* condition passes or {@code false} if it fails.
* @return the SpEL expression
*/
String value() default "true";
}
ConditionalOnExpression
注解上添加了另一个注解Conditional
,指明是哪个condition类处理改注解。
我们打开OnExpressionCondition
这个类:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
//获取到我们配置的EL表达式的值
String expression = (String) metadata
.getAnnotationAttributes(ConditionalOnExpression.class.getName())
.get("value");
expression = wrapIfNecessary(expression);
String rawExpression = expression;
expression = context.getEnvironment().resolvePlaceholders(expression);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
BeanExpressionResolver resolver = (beanFactory != null)
? beanFactory.getBeanExpressionResolver() : null;
BeanExpressionContext expressionContext = (beanFactory != null)
? new BeanExpressionContext(beanFactory, null) : null;
if (resolver == null) {
resolver = new StandardBeanExpressionResolver();
}
boolean result = (Boolean) resolver.evaluate(expression, expressionContext);
return new ConditionOutcome(result, ConditionMessage
.forCondition(ConditionalOnExpression.class, "(" + rawExpression + ")")
.resultedIn(result));
}
有这么一个方法,获取到我们设置的EL表达式的值后,进行一些处理,比如渠道environment中的值,然后计算整体的结果。
其中关键的是boolean result = (Boolean) resolver.evaluate(expression, expressionContext);
这么一行代码,通过springEL计算最终结果。
至此,spring的condition算是介绍完了,我们可以通过实现org.springframework.context.annotation.Condition#matches
来自定义condition。
实现不同环境加载不同的类,既可以使用ConditionalOnExpression
也可以使用@Profile
注解
两种都可以实现不同环境加载不同的类,写法不同,但是他们的实现原理都是一样的,都是获取环境变量spring.profiles.active的值,与value进行比较。读者可以去看一下
ProfileCondition
源码
@Bean
@Profile("dev")
public DevService initDevService1() {
log.info("DevService已加载---Profile");
return new DevService();
}
@Bean
@Profile("prod")
public ProdService initProdService1() {
log.info("ProdService已加载---Profile");
return new ProdService();
}
spring.factories文件里每一个xxxAutoConfiguration文件一般都会有下面的条件注解:
@ConditionalOnBean:当容器里有指定Bean的条件下
@ConditionalOnClass:当类路径下有指定类的条件下
@ConditionalOnExpression:基于SpEL表达式作为判断条件
@ConditionalOnJava:基于JV版本作为判断条件
@ConditionalOnJndi:在JNDI存在的条件下差在指定的位置
@ConditionalOnMissingBean:当容器里没有指定Bean的情况下
@ConditionalOnMissingClass:当类路径下没有指定类的条件下
@ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnResource:类路径是否有指定的值
@ConditionalOnSingleCandidate:当指定Bean在容器中只有一个,或者虽然有多个但是指定首选Bean
@ConditionalOnWebApplication:当前项目是Web项目的条件下。
上面@ConditionalOnXXX都是组合@Conditional元注解,使用了不同的条件Condition
springcloud系列学习笔记目录参见博主专栏 spring boot 2.X/spring cloud Greenwich。