外观模式介绍
外观模式也叫门面模式,它主要解决的是降低调用方使用接口时的复杂逻辑组合。在调用方与实际的接口提供方之间添加了一个中间层,向包装逻辑提供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仓库,工调用方引入。
总结
样例是通过中间件的方式实现外观模式,这种设计可以很好地增强代码的隔离性及复用性,不仅使用起来非常灵活,也降低了对每一个系统开发白名单拦截服务带来的风险及测试成本。当然设计模式讲求的是思想,而不是固定的实现方式。