spring 中对于 profiles 表达式的封装处理

1.profiles 表达式的用途

@Component
@Profile("prod | slow")
public class SlowProdEmployeeService implements EmployeeService{
}

以上代码只有在 prod 或者 slow 任意一个生效的时候才会注册组件

2.profiles 表达式的结构和作用

  • profiles 表达式由多个 profile 文件名字组成,并且支持简单逻辑运算符
  • profiles 表达式用户判断激活的配置文件是否满足表达式,比如激活的配置文件为 p1,表达式为 p1|p2,那就是满足的,如果表达式为 p1&p2,那就是不满足的
  • profiles 支持的逻辑运算有 &,|,!,(),四种

3. 实现分析

  • 首先 profile 表达式是由多个 profile 文件名字组成,然后它是要用来判断跟激活的配置文件的关系,换言之就是需要判断表达式中的每个 profile 是否是激活的配置文件,所以这里一定需要一个方法去判断
  • 在得到该配置文件的结果后,对这些结果进行一个逻辑运算,比如 p1&p2,假设 p1 和 p2,都是激活的配置文件,进行逻辑与运算后,如果依旧是 true,那就说明表达式是满足的
  • 最终 profile 表达式只要返回的是 true,那就是对的

4. spring 中的处理

4.1. 经过在上面的分析中,我们知道一定需要个方法判断是否是激活的配置文件,然后最终返回值是布尔值
4.2. Profiles 函数接口
@FunctionalInterface
public interface Profiles { 
	boolean matches(Predicate<String> activeProfiles); 
	static Profiles of(String... profiles) {
		return ProfilesParser.parse(profiles);
	} 
}
  • spring 中定义了这么一个接口用来处理我们上面说的逻辑,matches 方法用来解析表达式然后返回一个结果,参数为一个判断函数,也就是我们说的用来判断是否是激活的配置文件的方法,传入的是一个 string
  • Profiles.of 方法,这里我们暂时只需要知道存放的是解析逻辑就好,然后参数是一个字符串,也就是表达式,这里是支持多个表达式的,但是实际使用我们都是传入一个,实现逻辑是调用了一个解析方法,然后传入了表达式
4.3. ProfilesParser 解析表达式
static Profiles parse(String... expressions) {
	Assert.notEmpty(expressions, "Must specify at least one profile");
	Profiles[] parsed = new Profiles[expressions.length];
	for (int i = 0; i < expressions.length; i++) {
		parsed[i] = parseExpression(expressions[i]);
	}
	return new ParsedProfiles(expressions, parsed);
}
private static Profiles parseExpression(String expression) {
	Assert.hasText(expression, () -> "Invalid profile expression [" + expression + "]: must contain text");
	StringTokenizer tokens = new StringTokenizer(expression, "()&|!", true);
	return parseTokens(expression, tokens);
}
private static Profiles parseTokens(String expression, StringTokenizer tokens) {
	return parseTokens(expression, tokens, Context.NONE);
}

这段代码比较简单,就是把表达式,切割一下,与运算符分开,接下来遍历的时候得到的要么是某个配置文件名,要么就是某个运算符,最终是调用到一个 parseTokens 的方法,传入表达式和切割后的表达式集合(类似集合,可遍历),还有一个标识,然后返回的是一个 Profiles,也就是一个函数,注意在返回的是接口或者说函数时,该函数还没有执行,这也是函数编程最容易混淆的地方,函数真正执行,比如这里的 Profiles ,是在它调用了 matches 方法之后,这里我们就理解为返回了一个解析函数,只要执行了这个函数,就可以进行解析然后得到我们要的结果

private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) {
		// 存放对所有表达式要进行的操作
		List<Profiles> elements = new ArrayList<>();
		Operator operator = null;
		while (tokens.hasMoreTokens()) {
			String token = tokens.nextToken().trim();
			if (token.isEmpty()) {
				continue;
			}
			switch (token) {
				case "(":
					Profiles contents = parseTokens(expression, tokens, Context.BRACKET);
					// 因为取反操作时已经 add 过了,所以这里就不能 add
					if (context == Context.INVERT) {
						return contents;
					}
					elements.add(contents);
					break;
				case "&":
					// 判断前面有 & 了就必须继续为 &,即表达式不能出现  A|B&C
					assertWellFormed(expression, operator == null || operator == Operator.AND);
					// 标识一下,表示下一步要执行 与 操作,比如 p1&p2,当遇到 & 时,标识一下要进行 & 操作,再遍历到下一个时 p2,就对 p1 和 p2 进行与的判断
					operator = Operator.AND;
					break;
				case "|":
					assertWellFormed(expression, operator == null || operator == Operator.OR);
					operator = Operator.OR;
					break;
				case "!":
					// 当遇到取反操作时就将后面的表达式作为一个整体单独计算,not 方法会对后面的表达式执行结果后进行一个取反
					elements.add(not(parseTokens(expression, tokens, Context.INVERT)));
					break;
				case ")":
					Profiles merged = merge(expression, elements, operator);
					// 得到括号中需要进行的所有操作
					if (context == Context.BRACKET) {
						return merged;
					}
					elements.clear();
					elements.add(merged);
					operator = null;
					break;
				default:
					Profiles value = equals(token);
					if (context == Context.INVERT) {
						return value;
					}
					elements.add(value);
			}
		}
		return merge(expression, elements, operator);
	}

- 相等运算
1)这段代码我们首先关注到非运算符的情况,那就只有一种,也就是 default 的情况,这里的代码是调用了一个 equals,如下,我们看到传入的是一个配置文件名,返回的依旧是一个函数,这个函数是 profiles,那它的参数我们就知道了是一个判断函数,然后逻辑就是使用这个判断函数去判断传入的这个配置文件是否是激活的配置文件,也就是说 equals 方法,返回一个判断是否是激活配置文件的函数
ps: 需要额外注意的是,这里函数内部使用了我们传进来的外部变量 profile,此时就相当于一个闭包,这个外部变量就被存储在这个函数中了,只要我们调用了这个函数,就可以使用到这个变量,跟我们一般的编程,调用函数的时候再传入所有需要的变量是由区别的
2)最终得到的函数会存放到一个 list 中,我们知道 list 是有序的,所以我们到时候就可以按顺序执行这些函数

private static Profiles equals(String profile) {
	// 执行判断操作
	return activeProfile -> activeProfile.test(profile);
}

- 逻辑与或运算

case "|":
	assertWellFormed(expression, operator == null || operator == Operator.OR);
	operator = Operator.OR;
break;

1)这里首先做一个判断,然后标志当前运算符为 |,这个判断就是说当标识了运算符为 | 时,就不能出现其它运算符,这也是合理的,比如我我们 p1|p2|p3,是没有问题的,但是 p1|p2&p3,通常来讲这样写都不好,我们一般会加括号,这里直接就不考虑什么从左到右的逻辑了

private static Profiles merge(String expression, List<Profiles> elements, @Nullable Operator operator) {
	assertWellFormed(expression, !elements.isEmpty());
	if (elements.size() == 1) {
		return elements.get(0);
	}
	Profiles[] profiles = elements.toArray(new Profiles[0]);
	return (operator == Operator.AND ? and(profiles) : or(profiles));
}

2)在执行了这些逻辑后,我们就可以看最终的代码了,至于其它的运算符,都使用了递归,所以可以先不看,这段代码传入了我们得到的所以解析函数数组,假设不考虑其它运算符,那就是对表达式中的每个函数的判断逻辑,然后之类考虑了与还是非,接着往下看

// 执行存储的所有操作函数,只有全部是对的才是对的
private static Profiles and(Profiles... profiles) {
	return activeProfile -> Arrays.stream(profiles).allMatch(isMatch(activeProfile));
}

3)以与来说,比如 p1&p2,那就是我们必须判断 p1 是激活的,p2 也是激活的,这里很精髓的用了一个 allMatch 方法,也就是传入一个所有都返回 true,跟我们的逻辑刚好是对应的,然后是 isMatch 方法

private static Predicate<Profiles> isMatch(Predicate<String> activeProfile) {
// 执行单个 Profile
	return profiles -> profiles.matches(activeProfile);
}

4 ) 首先返回依旧是一个函数,这个函数就是执行单个的 Profiles,所以我们总结一下就是 and 方法就是执行存储的所有函数,然后希望它返回的是全部是 true,同理 or 的逻辑用了 anyMatch 方法

- 逻辑非运算
首先我们很容易就可以思考到对表达式的结果取反,我们先不考虑(),只有两种情况,一种就是连续的
取反 !!,一种就是对后面的一个 profile 取反,也就是 !p1

case "!":  
	elements.add(not(parseTokens(expression, tokens, Context.INVERT)));
	break;
private static Profiles not(Profiles profiles) {
	// 执行当前的操作,然后取反
	return activeProfile -> !profiles.matches(activeProfile);
}	

1)这里用到了递归,非常巧妙的解决了我们上面说的所有情况,假设不是递归,我们可能会这么做,当遍历到非运算时,我们标识以下,那如果接下来是配置文件,我们就会进行一个判断,也就是对 equals 方法取反,如果还是非,我们会取消标识继续遍历,类似下面这样

case "!":
if (context == Context.INVERT) {
	context = Context.NONE;
} else {
	context = Context.INVERT;
} 
break;
default:
Profiles value = equals(token);
if (context == Context.INVERT) {
	elements.add(not(value));
	context = Context.NONE;
} else {
	elements.add(value);
}

2)可以看到这种方法,不仅要来回消除运算符,还多了很多 if/else,肯定没有 spirng 写的好,接下来我们看下 spring 时怎么做的,

// 首先同样时要标识处于非的情况,将结果进行取反,然后假设是遇到连续非的,那反正你非几次我就做几次取反
case "!":
// 当遇到取反操作时就将后面的表达式作为一个整体单独计算,not 方法会对后面的表达式执行结果后进行一个取反
elements.add(not(parseTokens(expression, tokens, Context.INVERT)));
break;
// 然后是非后面的表达式,直接把结果 return 出去,然后递归不就结束了,这个时候对函数的结果取反不就是我们要的,组后 add 进去,那原本我们放进去的是一个判断是否激活的操作,现在就变成了判断后再取反的操作
Profiles value = equals(token);
if (context == Context.INVERT) {
	return value;
}

- 括号运算
一看到括号,毫无疑问肯定要递归,然后得到一个子表达式的结果

case "(":
Profiles contents = parseTokens(expression, tokens, Context.BRACKET); 
if (context == Context.INVERT) {
	return contents;
}
elements.add(contents);
break;
case ")":
Profiles merged = merge(expression, elements, operator);
// 得到括号中需要进行的所有操作
if (context == Context.BRACKET) {
	return merged;
}
elements.clear();
elements.add(merged);
operator = null;
break;

1)这里就非常好理解了,首先是遇到左括号我标识以下,遇到右括号时,我就判断当前是不是处于左括号内,如果时那就是递归结束了,所以我直接返回结果就行了,要得到表达式结果最终就是进行 merge 运算,这里如果再优化,可以考虑把 merge 也优化进递归中
2)最后是一个括号跟非得边界情况,有且只有一种就是递归的时候同时使用了括号,即 !(),当标识非,再遇到括号时,首先遇到非,也就是得到一个把后面的 () 当个整体运算,最后取反,跟 !p1,其实是一样的道理,运算 p1 时,假设是非,那就返回结果让非去取反,!(),那就运算括号内容,返回结果,让非去取反

4.3. 最终结果

就是一个 Profile,里面存了所有的运算顺序,怎么理解,这里我们用一个简单例子来说明,比如 p1&p2,
按照我们的解析逻辑
1)第一步是 p1,这个时候我们存放了一个判断 p1 是否激活的逻辑,
2)第二步是 &,我们标识一下
3)第三步是 p2,最终存放一个判断 p2 是否激活的逻辑
然后是合并逻辑,判断标识是 &,那就是所有的逻辑都只需要是 true 就行,也就是说当我们执行这个合并后的最终的 Profile 时,就是对表达式进行计算了,这个时候只需要外部传入一个判断函数进来

4.4 ParsedProfiles 存放解析函数
private static class ParsedProfiles implements Profiles {

		private final Set<String> expressions = new LinkedHashSet<>();

		private final Profiles[] parsed;

		ParsedProfiles(String[] expressions, Profiles[] parsed) {
			Collections.addAll(this.expressions, expressions);
			this.parsed = parsed;
		}

		@Override
		public boolean matches(Predicate<String> activeProfiles) {
			for (Profiles candidate : this.parsed) {
				if (candidate.matches(activeProfiles)) {
					return true;
				}
			}
			return false;
		}
}

在我们得到最终的 Profile 时,把它放进了一个对象,这个对象有且只有一个方法,该方法的逻辑就是执行存放的最终的 Profile(这里循环是多个的情况,前面说了不考虑),也就是说通过 Profiles.of 我们最终得到了一个存放解析函数的对象,里面有一个方法传入判断函数就可以得到表达式解析的结果

5.传入判断函数

经过前面的分析,在我们得到这个 ParsedProfiles 对象时,所有的逻辑就已经做好了,剩下的就是传判断函数了,这里就涉及对 Enviroment 的封装了,但是不重要,反正我们知道就是一个判断激活的函数就行,spring 中是这样传入,核心方法就是这个 AbstractEnvironment.aceeptsProifles

@Override
public boolean acceptsProfiles(Profiles profiles) {
	Assert.notNull(profiles, "Profiles must not be null");
	return profiles.matches(this::isProfileActive);
}
protected boolean isProfileActive(String profile) {
validateProfile(profile);
	Set<String> currentActiveProfiles = doGetActiveProfiles();
	return (currentActiveProfiles.contains(profile) ||
			(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
}

注意这里的 this::isProfileActive 不是调用,而是返回一个函数,写法同 profile->this.isProfileActive(profiel)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值