作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!😄
你感受到的容易,一定有人为你承担不容易
这句话更像是描述生活的,许许多多的磕磕绊绊总有人为你提供躲雨的屋檐和避风的港湾。其实编程开发的团队中也一样有人只负责CRUD中的简单调用,去使用团队中高级程序员开发出来的核心服务和接口。这样的编程开发对于初期刚进入程序员行业的小伙伴来说锻炼锻炼还是不错的,但随着开发的日子越来越久一直做这样的事情就很难得到成长,也想努力的去做一些更有难度的承担,以此来增强个人的技术能力。
没有最好的编程语言,语言只是工具
刀枪棍棒、斧钺钩叉、包子油条、盒子麻花,是语言。五郎八卦棍、十二路弹腿、洪家铁线拳,是设计。记得叶问里有一句台词是:金山找:今天我北方拳术,输给你南方拳术了。叶问:你错了,不是南北拳的问题,是你的问题。
所以当你编程开发写的久了,就不会再特别在意用的语言,而是为目标服务,用最好的设计能力也就是编程的智慧做出做最完美的服务。这也就是编程人员的价值所在!
设计与反设计以及过渡设计
设计模式是解决程序中不合理、不易于扩展、不易于维护的问题,也是干掉大部分ifelse
的利器,在我们常用的框架中基本都会用到大量的设计模式来构建组件,这样也能方便框架的升级和功能的扩展。但!如果不能合理的设计以及乱用设计模式,会导致整个编程变得更加复杂难维护,也就是我们常说的;反设计
、过渡设计
。而这部分设计能力也是从实践的项目中获取的经验,不断的改造优化摸索出的最合理的方式,应对当前的服务体量。
bugstack虫洞栈
,回复源码下载
获取(打开获取的链接,找到序号18)工程 | 描述 |
---|---|
itstack-demo-design-10-00 | 场景模拟工程;模拟一个提供接口服务的SpringBoot工程 |
itstack-demo-design-10-01 | 使用一坨代码实现业务需求 |
itstack-demo-design-10-02 | 通过设计模式开发为中间件,包装通用型核心逻辑 |
外观模式也叫门面模式,主要解决的是降低调用方的使用接口的复杂逻辑组合。这样调用方与实际的接口提供方提供方提供了一个中间层,用于包装逻辑提供API接口。有些时候外观模式也被用在中间件层,对服务中的通用性复杂逻辑进行中间件层包装,让使用方可以只关心业务开发。
那么这样的模式在我们的所见产品功能中也经常遇到,就像几年前我们注册一个网站时候往往要添加很多信息,包括;姓名、昵称、手机号、QQ、邮箱、住址、单身等等,但现在注册成为一个网站的用户只需要一步即可,无论是手机号还是微信也都提供了这样的登录服务。而对于服务端应用开发来说以前是提供了一个整套的接口,现在注册的时候并没有这些信息,那么服务端就需要进行接口包装,在前端调用注册的时候服务端获取相应的用户信息(从各个渠道),如果获取不到会让用户后续进行补全(营销补全信息给奖励),以此来拉动用户的注册量和活跃度。
在本案例中我们模拟一个将所有服务接口添加白名单的场景
在项目不断壮大发展的路上,每一次发版上线都需要进行测试,而这部分测试验证一般会进行白名单开量或者切量的方式进行验证。那么如果在每一个接口中都添加这样的逻辑,就会非常麻烦且不易维护。另外这是一类具备通用逻辑的共性需求,非常适合开发成组件,以此来治理服务,让研发人员更多的关心业务功能开发。
一般情况下对于外观模式的使用通常是用在复杂或多个接口进行包装统一对外提供服务上,此种使用方式也相对简单在我们平常的业务开发中也是最常用的。你可能经常听到把这两个接口包装一下,但在本例子中我们把这种设计思路放到中间件层,让服务变得可以统一控制。
itstack-demo-design-10-00 └── src ├── main │ ├── java │ │ └── org.itstack.demo.design │ │ ├── domain │ │ │ └── UserInfo.java │ │ ├── web │ │ │ └── HelloWorldController.java │ │ └── HelloWorldApplication.java │ └── resources │ └── application.yml └── test └── java └── org.itstack.demo.test └── ApiTest.java
SpringBoot
的HelloWorld
工程,在工程中提供了查询用户信息的接口HelloWorldController.queryUserInfo
,为后续扩展此接口的白名单过滤做准备。@RestController public class HelloWorldController { @Value("${server.port}") private int port; /** * key:需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用 * returnJson:预设拦截时返回值,是返回对象的Json * * http://localhost:8080/api/queryUserInfo?userId=1001 * http://localhost:8080/api/queryUserInfo?userId=小团团 */ @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET) public UserInfo queryUserInfo(@RequestParam String userId) { return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号"); } }
userId
,查询用户信息。后续就需要在这里扩展白名单,只有指定用户才可以查询,其他用户不能查询。@SpringBootApplication @Configuration public class HelloWorldApplication { public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } }
SpringBoot
启动类。需要添加的是一个配置注解@Configuration
,为了后续可以读取白名单配置。一般对于此种场景最简单的做法就是直接修改代码
累加if
块几乎是实现需求最快也是最慢的方式,快是修改当前内容很快,慢是如果同类的内容几百个也都需要如此修改扩展和维护会越来越慢。
itstack-demo-design-10-01 └── src └── main └── java └── org.itstack.demo.design └── HelloWorldController.java
public class HelloWorldController { public UserInfo queryUserInfo(@RequestParam String userId) { // 做白名单拦截 List<String> userList = new ArrayList<String>(); userList.add("1001"); userList.add("aaaa"); userList.add("ccc"); if (!userList.contains(userId)) { return new UserInfo("1111", "非白名单可访问用户拦截!"); } return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号"); } }
接下来使用外观器模式来进行代码优化,也算是一次很小的重构。
这次重构的核心是使用外观模式也可以说门面模式,结合SpringBoot
中的自定义starter
中间件开发的方式,统一处理所有需要白名单的地方。
后续接下来的实现中,会涉及的知识;
itstack-demo-design-10-02 └── src ├── main │ ├── java │ │ └── org.itstack.demo.design.door │ │ ├── annotation │ │ │ └── DoDoor.java │ │ ├── config │ │ │ ├── StarterAutoConfigure.java │ │ │ ├── StarterService.java │ │ │ └── StarterServiceProperties.java │ │ └── DoJoinPoint.java │ └── resources │ └── META_INF │ └── spring.factories └── test └── java └── org.itstack.demo.test └── ApiTest.java
门面模式模型结构
public class StarterService { private String userStr; public StarterService(String userStr) { this.userStr = userStr; } public String[] split(String separatorChar) { return StringUtils.split(this.userStr, separatorChar); } }
@ConfigurationProperties("itstack.door") public class StarterServiceProperties { private String userStr; public String getUserStr() { return userStr; } public void setUserStr(String userStr) { this.userStr = userStr; } }
application.yml
中添加 itstack.door
的配置信息。@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
、@EnableConfigurationProperties
,这一部分主要是与SpringBoot的结合使用。@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DoDoor { String key() default ""; String returnJson() default ""; }
@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)
,接下来我们分别介绍。@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
定义切面,这里采用的是注解路径,也就是所有的加入这个注解的方法都会被切面进行管理。
getFiledValue
获取指定key也就是获取入参中的某个属性,这里主要是获取用户ID,通过ID进行拦截校验。
returnObject
返回拦截后的转换对象,也就是说当非白名单用户访问时则返回一些提示信息。
doRouter
切面核心逻辑,这一部分主要是判断当前访问的用户ID是否白名单用户,如果是则放行jp.proceed();
,否则返回自定义的拦截提示信息。
这里的测试我们会在工程:itstack-demo-design-10-00
中进行操作,通过引入jar包,配置注解的方式进行验证。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>itstack-demo-design-10-02</artifactId> </dependency>
# 自定义中间件配置 itstack: door: enabled: true userStr: 1001,aaaa,ccc #白名单用户ID,多个逗号隔开
/** * http://localhost:8080/api/queryUserInfo?userId=1001 * http://localhost:8080/api/queryUserInfo?userId=小团团 */ @DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}") @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET) public UserInfo queryUserInfo(@RequestParam String userId) { return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号"); }
@DoDoor
,也就是我们的外观模式中间件化实现。. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.2.RELEASE) 2020-06-11 23:56:55.451 WARN 65228 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration) 2020-06-11 23:56:55.531 INFO 65228 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2020-06-11 23:56:55.533 INFO 65228 --- [ main] o.i.demo.design.HelloWorldApplication : Started HelloWorldApplication in 1.688 seconds (JVM running for 2.934)
白名单用户访问
http://localhost:8080/api/queryUserInfo?userId=1001
{"code":"0000","info":"success","name":"虫虫:1001","age":19,"address":"天津市南开区旮旯胡同100号"}
非白名单用户访问
http://localhost:8080/api/queryUserInfo?userId=小团团
{"code":"1111","info":"非白名单可访问用户拦截!","name":null,"age":null,"address":null}
userId
换成小团团
,此时返回的信息已经是被拦截的信息。而这个拦截信息正式我们自定义注解中的信息:@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")