外观模式介绍
外观模式也叫门面模式,它主要解决的是降低调用方使用接口时的复杂逻辑组合。在调用方与实际的接口提供方之间添加了一个中间层,向包装逻辑提供API接口。有时外观模式也被用在中间件层,用服务中的通用性复杂逻辑包装中间件层,让使用方可以只关心业务,简化调用。
外观模式简单模拟
模拟场景:中间件
在项目不断壮大发展的过程中,每一次发版上线都需要测试,而这部分测试验证一般会通过白名单开量或切量的方式验证。如果在每一个接口中都添加这种逻辑,就会非常麻烦且不易维护。另外,这时一类具备通用逻辑的共性需求,非常适合开发成组件,一次治理服务,从而让研发人员可以将精力放在业务功能逻辑的开发上。
场景模拟工程
这是一个SpringBoot的工程,在工程中提供了查询用户信息的接口,为后续扩展此接口的白名单过滤做准备。
1 2 3 4 5 6 7 8 9 10 11
|
@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
|
@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不同,对于此类方式获取白名单配置存在差异
配置服务类
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仓库,工调用方引入。
总结
样例是通过中间件的方式实现外观模式,这种设计可以很好地增强代码的隔离性及复用性,不仅使用起来非常灵活,也降低了对每一个系统开发白名单拦截服务带来的风险及测试成本。当然设计模式讲求的是思想,而不是固定的实现方式。