重学 Java 设计模式:实战外观模式「基于SpringBoot开发门面模式中间件,统一控制接口白名单场景」
作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!
一、前言
你感受到的容易,一定有人为你承担不容易
这句话更像是描述生活的,许许多多的磕磕绊绊总有人为你提供躲雨的屋檐和避风的港湾。其实编程开发的团队中也一样有人只负责CRUD中的简单调用,去使用团队中高级程序员开发出来的核心服务和接口。这样的编程开发对于初期刚进入程序员行业的小伙伴来说锻炼锻炼还是不错的,但随着开发的日子越来越久一直做这样的事情就很难得到成长,也想努力的去做一些更有难度的承担,以此来增强个人的技术能力。
没有最好的编程语言,语言只是工具
刀枪棍棒、斧钺钩叉、包子油条、盒子麻花,是语言。五郎八卦棍、十二路弹腿、洪家铁线拳,是设计。记得叶问里有一句台词是:金山找:今天我北方拳术,输给你南方拳术了。叶问:你错了,不是南北拳的问题,是你的问题。
所以当你编程开发写的久了,就不会再特别在意用的语言,而是为目标服务,用最好的设计能力也就是编程的智慧做出做最完美的服务。这也就是编程人员的价值所在!
设计与反设计以及过渡设计
设计模式是解决程序中不合理、不易于扩展、不易于维护的问题,也是干掉大部分ifelse
的利器,在我们常用的框架中基本都会用到大量的设计模式来构建组件,这样也能方便框架的升级和功能的扩展。但!如果不能合理的设计以及乱用设计模式,会导致整个编程变得更加复杂难维护,也就是我们常说的;反设计
、过渡设计
。而这部分设计能力也是从实践的项目中获取的经验,不断的改造优化摸索出的最合理的方式,应对当前的服务体量。
二、开发环境
- JDK 1.8
- Idea + Maven
- SpringBoot 2.1.2.RELEASE
- 涉及工程三个,可以通过关注公众号:
bugstack虫洞栈
,回复源码下载
获取(打开获取的链接,找到序号18)
工程 | 描述 |
---|---|
itstack-demo-design-10-00 | 场景模拟工程;模拟一个提供接口服务的SpringBoot工程 |
itstack-demo-design-10-01 | 使用一坨代码实现业务需求 |
itstack-demo-design-10-02 | 通过设计模式开发为中间件,包装通用型核心逻辑 |
三、外观模式介绍
外观模式也叫门面模式,主要解决的是降低调用方的使用接口的复杂逻辑组合。这样调用方与实际的接口提供方提供方提供了一个中间层,用于包装逻辑提供API接口。有些时候外观模式也被用在中间件层,对服务中的通用性复杂逻辑进行中间件层包装,让使用方可以只关心业务开发。
那么这样的模式在我们的所见产品功能中也经常遇到,就像几年前我们注册一个网站时候往往要添加很多信息,包括;姓名、昵称、手机号、QQ、邮箱、住址、单身等等,但现在注册成为一个网站的用户只需要一步即可,无论是手机号还是微信也都提供了这样的登录服务。而对于服务端应用开发来说以前是提供了一个整套的接口,现在注册的时候并没有这些信息,那么服务端就需要进行接口包装,在前端调用注册的时候服务端获取相应的用户信息(从各个渠道),如果获取不到会让用户后续进行补全(营销补全信息给奖励),以此来拉动用户的注册量和活跃度。
四、案例场景模拟
在本案例中我们模拟一个将所有服务接口添加白名单的场景
在项目不断壮大发展的路上,每一次发版上线都需要进行测试,而这部分测试验证一般会进行白名单开量或者切量的方式进行验证。那么如果在每一个接口中都添加这样的逻辑,就会非常麻烦且不易维护。另外这是一类具备通用逻辑的共性需求,非常适合开发成组件,以此来治理服务,让研发人员更多的关心业务功能开发。
一般情况下对于外观模式的使用通常是用在复杂或多个接口进行包装统一对外提供服务上,此种使用方式也相对简单在我们平常的业务开发中也是最常用的。你可能经常听到把这两个接口包装一下,但在本例子中我们把这种设计思路放到中间件层,让服务变得可以统一控制。
1. 场景模拟工程
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
,为后续扩展此接口的白名单过滤做准备。
2. 场景简述
2.1 定义基础查询接口
@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
,查询用户信息。后续就需要在这里扩展白名单,只有指定用户才可以查询,其他用户不能查询。
2.2 设置Application启动类
@SpringBootApplication
@Configuration
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
}
- 这里是通用的
SpringBoot
启动类。需要添加的是一个配置注解@Configuration
,为了后续可以读取白名单配置。
五、用一坨坨代码实现
一般对于此种场景最简单的做法就是直接修改代码
累加if
块几乎是实现需求最快也是最慢的方式,快是修改当前内容很快,慢是如果同类的内容几百个也都需要如此修改扩展和维护会越来越慢。
1. 工程结构
itstack-demo-design-10-01
└── src
└── main
└── java
└── org.itstack.demo.design
└── HelloWorldController.java
- 以上的实现是模拟一个Api接口类,在里面添加白名单功能,但类似此类的接口会有很多都需要修改,所以这也是不推荐使用此种方式的重要原因。
2. 代码实现
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
中间件开发的方式,统一处理所有需要白名单的地方。
后续接下来的实现中,会涉及的知识;
- SpringBoot的starter中间件开发方式。
- 面向切面编程和自定义注解的使用。
- 外部自定义配置信息的透传,SpringBoot与Spring不同,对于此类方式获取白名单配置存在差异。
1. 工程结构
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
门面模式模型结构
- 以上是外观模式的中间件实现思路,右侧是为了获取配置文件,左侧是对于切面的处理。
- 门面模式可以是对接口的包装提供出接口服务,也可以是对逻辑的包装通过自定义注解对接口提供服务能力。
2. 代码实现
2.1 配置服务类
public class StarterService {
private String userStr;
public StarterService(String userStr) {
this.userStr = userStr;
}
public String[] split(String separatorChar) {
return StringUtils.split(this.userStr, separatorChar);
}
}
- 以上类的内容较简单只是为了获取配置信息。
2.2 配置类注解定义
@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
的配置信息。
2.3 自定义配置类信息获取
@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的结合使用。
2.4 切面注解定义
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
String key() default "";
String returnJson() default "";
}
- 定义了外观模式门面注解,后续就是此注解添加到需要扩展白名单的方法上。
- 这里提供了两个入参,key:获取某个字段例如用户ID、returnJson:确定白名单拦截后返回的具体内容。
2.5 白名单切面逻辑
@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();
,否则返回自定义的拦截提示信息。
3. 测试验证
这里的测试我们会在工程:itstack-demo-design-10-00
中进行操作,通过引入jar包,配置注解的方式进行验证。
3.1 引入中间件POM配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
- 打包中间件工程,给外部提供jar包服务
3.2 配置application.yml
# 自定义中间件配置
itstack:
door:
enabled: true
userStr: 1001,aaaa,ccc #白名单用户ID,多个逗号隔开
- 这里主要是加入了白名单的开关和白名单的用户ID,逗号隔开。
3.3 在Controller中添加自定义注解
/**
* 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
,也就是我们的外观模式中间件化实现。 - key:需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用。
- returnJson:预设拦截时返回值,是返回对象的Json。
3.4 启动SpringBoot
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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)
- 启动正常,SpringBoot已经启动可以对外提供服务。
3.5 访问接口接口测试
白名单用户访问
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\":\"非白名单可访问用户拦截!\"}")
七、总结
- 以上我们通过中间件的方式实现外观模式,这样的设计可以很好的增强代码的隔离性,以及复用性,不仅使用上非常灵活也降低了每一个系统都开发这样的服务带来的风险。
- 可能目前你看这只是非常简单的白名单控制,是否需要这样的处理。但往往一个小小的开始会影响着后续无限的扩展,实际的业务开发往往也要复杂的很多,不可能如此简单。因而使用设计模式来让代码结构更加干净整洁。
- 很多时候不是设计模式没有用,而是自己编程开发经验不足导致即使学了设计模式也很难驾驭。毕竟这些知识都是经过一些实际操作提炼出来的精华,但如果你可以按照本系列文章中的案例方式进行学习实操,还是可以增强这部分设计能力的。
八、推荐阅读
- 1. 重学 Java 设计模式:实战工厂方法模式(多种类型商品发奖场景)
- 2. 重学 Java 设计模式:实战抽象工厂模式(替换Redis双集群升级场景)
- 3. 重学 Java 设计模式:实战建造者模式(装修物料组合套餐选配场景)
- 4. 重学 Java 设计模式:实战原型模式(多套试每人题目和答案乱序场景)
- 5. 重学 Java 设计模式:实战桥接模式(多支付渠道「微信、支付宝」与多支付模式「刷脸、指纹」场景)
- 6. 重学 Java 设计模式:实战组合模式(营销差异化人群发券,决策树引擎搭建场景)
重学 Java 设计模式:实战外观模式「基于SpringBoot开发门面模式中间件,统一控制接口白名单场景」的更多相关文章
- 重学 Java 设计模式:实战代理模式「模拟mybatis-spring中定义DAO接口,使用代理类方式操作数据库原理实现场景」
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 难以跨越的瓶颈期,把你拿捏滴死死的! 编程开发学习过程中遇到的瓶颈期,往往是由于看不 ...
- 重学 Java 设计模式:实战责任链模式「模拟618电商大促期间,项目上线流程多级负责人审批场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 场地和场景的重要性 射击
- 重学 Java 设计模式:实战迭代器模式「模拟公司组织架构树结构关系,深度迭代遍历人员信息输出场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 相信相信的力量! 从懵懂的少年,到拿起键盘,可以写一个Hell ...
- 重学 Java 设计模式:实战备忘录模式「模拟互联网系统上线过程中,配置文件回滚场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 实现不了是研发的借口? 实现不了,有时候是功能复杂度较高难以实 ...
- 重学 Java 设计模式:实战状态模式「模拟系统营销活动,状态流程审核发布上线场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! @ 目录 一.前言 二.开发环境 三.状态模式介绍 四.案例场景模拟 1 ...
- 重学 Java 设计模式:实战访问者模式「模拟家长与校长,对学生和老师的不同视角信息的访问场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 能力,是你前行的最大保障 年龄会不断的增长,但是什么才能让你不 ...
- 重学 Java 设计模式:实战享元模式「基于Redis秒杀,提供活动与库存信息查询场景」
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 程序员的上下文是什么? 很多时候一大部分编程开发的人员都只是关注于功能的实现,只 ...
- 重学 Java 设计模式:实战桥接模式(多支付渠道「微信、支付宝」与多支付模式「刷脸、指纹」场景)
作者:小傅哥 博客:https://bugstack.cn - 编写系列原创专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 为什么你的代码那么多ifelse 同类的业务.同样的功能, ...
- 重学 Java 设计模式:实战装饰器模式(SSO单点登录功能扩展,增加拦截用户访问方法范围场景)
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 对于代码你有编程感觉吗 很多人写代码往往是没有编程感觉的,也就是除了可以把功能按照固 ...
随机推荐
- MySQL索引及查询优化
mysql 索引 1.索引介绍 索引按数据结构分可分为哈希表,有序数组,搜索树,跳表: 哈希表适用于只有等值查询的场景 有序数组适用于有等值查询和范围查询的场景,但有序数组索引的更新代价很大,所以最好 ...
- scrapy中间件之下载中间件使用(网易新闻爬取)
scrapy项目中的middlewarse.py中间件 爬虫中间件:目前先不介绍 下载中间件(需要在settings.py中开启) (1)请求处理函数:process_request(self, re ...
- SSL F5
应用交付领域经常提到SSL加速,但SSL加速到底是什么意思?SSL加速和F5指什么意思呢?在网上查询和整理了一些关于SSL加速和F5的相关解释,仅供参考:SSL加速:加密套接层协议(简称SSL)是网络 ...
- poj3635 优先队列+打标记+广搜
After going through the receipts from your car trip through Europe this summer, you realised that th ...
- Robot Framework(3)- 基本概念
如果你还想从头学起Robot Framework,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1770899.html 前言 在 RF ...
- PHP基础-自定义函数-变量范围-函数参数传递
一.自定义函数 function 函数名([形式参数1,形式参数2,....形式参数n]){ //各种PHP代码.... //...... return ...
- 如何利用CSS选择器抓取京东网商品信息
前几天小编分别利用Python正则表达式.BeautifulSoup.Xpath分别爬取了京东网商品信息,今天小编利用CSS选择器来为大家展示一下如何实现京东商品信息的精准匹配~~ CSS选择器 目前 ...
- sql注入讲解
1.输入1' 发现数据库报错,原因是我们的输入直接被代入到数据库查询语句里面. 2.有没有办法可以不让他报错呢?可以尝试一下构造正确的数据库语法,使之不报错.比如输入 1 and 1=1 试试 sel ...
- BUUCTF Crypto
BUUCTF 几道crypto WP [AFCTF2018]Morse 简单的莫尔斯密码,最直观的莫尔斯密码是直接采用空格分割的点和划线,这题稍微绕了一下使用的是斜杠来划分 所以首先将斜杠全部替换为空 ...
- Codeforces Round #626 (Div. 2, based on Moscow Open Olympiad in Informatics)部分(A~E)题解
(A) Even Subset Sum Problem 题解:因为n非常非常小,直接暴力枚举所有区间即可. #include<bits/stdc++.h> using namespace ...