外观模式介绍

外观模式也叫门面模式,它主要解决的是降低调用方使用接口时的复杂逻辑组合。在调用方与实际的接口提供方之间添加了一个中间层,向包装逻辑提供API接口。有时外观模式也被用在中间件层,用服务中的通用性复杂逻辑包装中间件层,让使用方可以只关心业务,简化调用。

外观模式简单模拟

模拟场景:中间件

在项目不断壮大发展的过程中,每一次发版上线都需要测试,而这部分测试验证一般会通过白名单开量或切量的方式验证。如果在每一个接口中都添加这种逻辑,就会非常麻烦且不易维护。另外,这时一类具备通用逻辑的共性需求,非常适合开发成组件,一次治理服务,从而让研发人员可以将精力放在业务功能逻辑的开发上。

场景模拟工程

这是一个SpringBoot的工程,在工程中提供了查询用户信息的接口,为后续扩展此接口的白名单过滤做准备。

1
2
3
4
5
6
7
8
9
10
11
/**
* @author bestrookie
* @date 2021/11/15 9:19 上午
*/
@RestController
public class HelloWorldController {
@GetMapping("/query")
public UserInfo queryUserInfo(@RequestParam String userId){
return new UserInfo("rookie:"+userId,19,"山东省");
}
}

违背设计模式实现

最简单的做法是直接修改代码。if..else是实现需求最快的方式

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author bestrookie
* @date 2021/11/15 9:19 上午
*/
@RestController
public class HelloWorldController {
@GetMapping("/query")
public UserInfo queryUserInfo(@RequestParam String userId){
List<String> userList = new ArrayList<>();
userList.add("1001");
userList.add("aaaa");
userList.add("ccc");
if (!userList.contains(userId)){
return new UserInfo("1111","非白名单用户,已被拦截");
}
return new UserInfo("rookie:"+userId,19,"山东省");
}
}

从以上的实现方式可以看出,白名单的逻辑代码占据了一大块,但它不是业务功能流程中的逻辑,只是因为上线过程中需要在开量前测试验证。如果平时对待此类需求是用这样的方式解决的,那么可以按照此种设计模式进行优化,让后续的扩展和剔除更容易。

外观模式重构代码

这次重构的信息是使用外观模式,结合SpringBoot中自定义starter中间件的方式,统一处理所有需要开启白名单逻辑的代码。

在接下来的实现过程中,涉及到的知识点包括:

  • SpringBoot的starter中间件开发方式
  • 面向切面变成和自定义注解的使用方式
  • 外部自定义配置信息的透传。SpringBoot与Spring不同,对于此类方式获取白名单配置存在差异

image-20211115222427286

配置服务类
1
2
3
4
5
6
7
8
9
10
11
12
public class StarterService {

private String userStr;

public StarterService(String userStr) {
this.userStr = userStr;
}

public String[] split(String separatorChar) {
return this.userStr.split(separatorChar);
}
}

配置服务类的内容比较简单,知识为了获取SpringBoot中配置文件的信息内容

配置类注解定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {

private String userStr;

public String getUserStr() {
return userStr;
}

public void setUserStr(String userStr) {
this.userStr = userStr;
}

}

配置类主角用于定义后续在application.yaml中添加israck.door的配置信息。

获取自定义配置类信息
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {
@Autowired
private StarterServiceProperties properties;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
StarterService starterService() {
return new StarterService(properties.getUserStr());
}
}

以上代码是获取配置的过程,主要是对注解@Configuration、@ConditionalOnClass、@EnableConfigurationProperies的定义,这一部分主要是与SpringBoot的结合使用方法。

切面注解定义
1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
String key() default "";
String returnJson() default "";
}

切面注解定义了外观模式切片注解,后续将此注解添加到需要扩展白名单的方法上。这里提供了两个入参:key获取某个字段,例如用户ID;returnJson确定白名单拦截后返回的具体内容,

白名单切面逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Aspect
@Component
public class DoJoinPoint {

private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);

@Autowired
private StarterService starterService;

@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
public void aopPoint() {
}

@Around("aopPoint()")
public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
//获取内容
Method method = getMethod(jp);
DoDoor door = method.getAnnotation(DoDoor.class);
//获取字段值
String keyValue = getFiledValue(door.key(), jp.getArgs());
logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
if (null == keyValue || "".equals(keyValue)) return jp.proceed();
//配置内容
String[] split = starterService.split(",");
//白名单过滤
for (String str : split) {
if (keyValue.equals(str)) {
return jp.proceed();
}
}
//拦截
return returnObject(door, method);
}

private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}

private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
return jp.getTarget().getClass();
}

//返回对象
private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
Class<?> returnType = method.getReturnType();
String returnJson = doGate.returnJson();
if ("".equals(returnJson)) {
return returnType.newInstance();
}
return JSON.parseObject(returnJson, returnType);
}

//获取属性值
private String getFiledValue(String filed, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
if (null == filedValue || "".equals(filedValue)) {
filedValue = BeanUtils.getProperty(arg, filed);
} else {
break;
}
} catch (Exception e) {
if (args.length == 1) {
return args[0].toString();
}
}
}
return filedValue;
}
}

这里面包括的内容比较多,核心逻辑主要是主要是Object doRouter(ProceedingJoinPoint jp),接下来分别介绍。

1、@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")

定义切面,这里采用的是注解路径,也就是所有加入这个注解的方法都会被切面管理。

2、getFiledValue

获取指定的key,也就是获取入参中的某个属性,这里主要是获取用户ID,通过ID拦截校验

3、returnObject

返回拦截后的转换对象,当非白名单用户访问时,会返回一些提示信息

4、doRouter

切面核心逻辑,这部分主要是判断当前访问的用户ID是否为白名单用户。如果是则放行jp.proceed();否则返回自定义的拦截提示信息。

这就完成一个简单的中间件开发,我们讲这个工程打包,给外部提供jar包服务。在实际的开发中,将这种jar包上传到Maven仓库,工调用方引入。

总结

样例是通过中间件的方式实现外观模式,这种设计可以很好地增强代码的隔离性及复用性,不仅使用起来非常灵活,也降低了对每一个系统开发白名单拦截服务带来的风险及测试成本。当然设计模式讲求的是思想,而不是固定的实现方式。

评论