一哥你好,之前有在群里和前几节课听到你说的一些关于接口的应用,我自己十分有感触,所以在这里分享自己在开发中的经验,第一次写分享,格式和措辞可能很差,就当做抛砖引玉了吧。
先说一下自己的背景吧,我是18年年初入职一家创业公司,公司只有一个开发经理,项目小而且多,所以对于我这菜鸟的要求就是项目能跑就行,而且想用什么技术都由自己决定。
1、项目开发脚手架
开发了两三个月左右,我对枯燥的crud十分头疼,代码里总是千篇一律的entity-repository-service-controller。就拿controller中普通的列表来说吧,参数传进来,对所有的参数进行判空,拼接条件,然后返回分页数据。而添加数据则是对入参进行各种判断,然后保存。当时就想着是不是有什么办法可以免去这些重复的工作,但由于刚入职,也不是很敢去尝试。直到我接手了一个二开项目,很吃力的阅读了三个礼拜左右,发现它的开发模式竟然和我之前的想法不谋而合,而且写得相当漂亮。于是我立刻着手去做我们公司第一版开发脚手架。
首先定义了含有基本属性的entity超类,包含id,name,createTime等属性,repository接口则采用泛型<T extends MyEntity>
作为entity的声明,并继承了jpa的JpaRepository,service的超类则是手动写了一些基础的crud,而controller就比较麻烦了。就像我之前说的,如何在超类就解决列表-添加数据-修改数据之前的操作呢。
首先是列表,动态条件的构建首先让我想到的是反射,通过制定condition规范(入参参数名必须与实体类参数名一致),将入参的参数名与实体类的参数名进行对应,动态构建JPA的specification。果然和预料的一样,之后再定义condition的超类(包含页码、页面大小、排序字段等),通用列表功能就解决了。当然此处有个缺点,这只能运用于equal的操作,我如何对比较,like进行精确定位?大概是框架运行了大半年,我用注解解决了这一问题。
其次是添加,添加的痛点在于各种入参判断,如何动态判断各个参数?这时候spring-boot validation就帮上忙了,只要在入参中添加注解,并在实体类中添加@valid注解就能动态判断了。
最后是修改(删除的通用操作难度不大),采用了Hutool的BeanCopy工具,然后用validation主动校验的工具进行参数校验。也算是完成了。
最后实现的效果就是,一个需求进来,我只需要定义实体类,然后repository,service,controller一路继承,只要不是特别复杂的需求,这些通用方法都能应付,甚至连表都是jpa创建的。
这是我职业生涯中对超类的第一次运用,只是普通的继承,相当粗浅,不过认为自己造了有用的轮子还是很有成就感的。当然实现起来肯定不像我说的这些这么简单,也是踩了相当多的坑。
2、自制validation框架
脚手架用了半年多,还是发现了很多问题。但我认为最有意义的一个就是发现了validation校验框架有时候会重复判断多次,影响到了当时的某个业务。我曾多次静下心跟着断点去追踪源码,但奈何水平不够,也不够耐心,没发现问题所在,索性就自己做一个吧。
我观察了validation的使用,其中包含了声明注解(我乱起的)@valid
,各种类型的判断注解@NotNull
等,以及相对应的这些注解的判断算法。思索了一两天后,框架的大概轮廓就出来了。
@valid
是指明该参数需要校验,这个通过百度,查到了参数拦截器(也是乱起的)HandlerMethodArgumentResolver
这么个东西。在个这方法下,通过反射我能获取到标记了@valid
注解实体类的参数名、值、以及注解。那么关键的问题来了,知道了具体的校验注解我如何根据注解实现校验?
第一个想到的方法就是if-else,然后我就给了自己一嘴巴子,那也太low太麻烦了。略微参考了下源码(虽然还是没看懂),发现了这些校验算法都指向了一个接口ConstraintValidator
。然后就联想到初学java继承时父和子的例子:
Father f = new Son();
f.method();//此处调用的是子类实现
这里情况略微不同,ConstraintValidator
是接口,其实很好理解,父类并不关心方法是如何实现的,只要指定这个方法的规则就行,子类爱咋实现无所谓。于是我的抽象思维就从这里开始了。
定义一个校验超类接口,校验的行为就是校验参数,由各个子类进行实现,而子类注解和子类算法需要进行绑定,也就是在知道某个注解的情况下能够精确地去调用这个注解的算法,如下图。
//省略部分声明注解
@Constraint(validatedBy = NotBlankValidate.class)//此处进行绑定算法
public @interface NotBlank {
String message();
Class<?>[] groups() default {};
boolean isTrim() default false;
}
最后实现的步骤就很清晰了,参数拦截器捕获带有@valid
实体,通过反射分解实体中每一个参数的参数名、值以及最重要的校验注解。获取到这些后,通过校验注解获取到校验算法,再调用算法中的校验方法,结束。
//获取校验算法类
Class validateClass = constraint.validatedBy();
//初始化校验算法类(以接口形式)
//此处可以一开始就初始化所有算法存在容器中去获取,效率会更高
AbstractValidate abstractValidate = (AbstractValidate)ReflectUtil.newInstance(validateClass);
//调用校验方法
boolean flag =abstractValidate.isValid();
这次自己造轮子狠狠叩开了我对抽象认识的大门,虽然造出来的是已有的轮子,而且效率方面可能比我好的多。
3、策略模式在业务中的运用
如果说自制框架是对我最有意义的一次coding,那么这一次策略模式的应用一定是最令我满意的一次。
背景是这样的,公司在做一款对接各个第三方通道的支付产品,一开始只有一两家第三方接口,所以if-else用起来简直不要太酸爽。但随着对接的第三方增多,我逐渐感受到了以前crud时带给我的烦躁情绪。我该如何摆脱对接完接口还要去给那一坨看着就引起不适的if-else再加一行else if()?检验框架那会的开发经验给我某种直觉,一定是有某种方法能够解决我目前的困境,让我在写完对接接口方法后就能直接让系统自己去精准识别和调用。写了几份demo后总算是有了大概的思路。
首先定义一个DefinedInterface
的接口,里边就一个方法,Integer defined()。相当于给所有子类打上了一个标签。
public interface DefinedInterface {
//此处EnumInterface是我自己定义的一个枚举的抽象,是之后优化的,一开始直接采用Integer粗暴指定
EnumInterface defined();
}
然后是定义支付方法抽象并继承之前的DefinedInterface接口
public interface PayInterface extends DefinedInterface {
// 通用主扫支付
WebPayVo webPay(CommonPayParam commonPayParam) throws Exception;
// 通用条码支付
MicroPayVo microPay(CommonPayParam commonPayParam) throws Exception;
// 通用支付查询
PayRes payQuery(CommonQueryParam commonQueryParam);
// 通用支付退款
PayRes payRefund(CommonRefundParam commonRefundParam);
// 通用微信小程序支付
default WebPayVo appletPay(CommonPayParam commonPayParam) throws Exception {
return new WebPayVo();
}
}
再来就是以各个第三方支付公司为单位,继承PayInterface
,按照父类指定的方法去逐个实现
最后就是调用,这个子类其实都是打上@service
注解的,也就是说是能够通过spring去获取到实现这个父类的子类集合的。
Map<String, PayInterface> map = ApplicationContext.getBeanOfType(PayInterface.class);
根据前端传上来的标识以及之前defined()
方法,很容易知道该调用哪个子类
//此处可以一开始就初始化所有算法存在容器中去获取,效率会更高(抄上边的)
for (PayInterface payInterface : payInterfaceMap.values()) {
if (order.getPayChannel().equals(payInterface.defined().getCode())) {
webPayVo = payInterface.appletPay(commonPayParam);
}
}
这样就达到的效果就是,一个第三方公司的接口过来,我只要根据支付父类实现子类,对接好接口,就算完成了一次功能的对接。实际开发会更加复杂,因为不仅仅是多个第三方公司接口的存在,还有支付宝、微信、银行卡等支付类型,所以这里进行了简化,对这些支付类型不做讨论。
经历过这次开发以后,偶然的一次机会叕翻开了设计模式中的策略模式,意外的发现和我现在的思路是如此相似。其实设计模式以前也看过好几遍,不过基本上是看完就忘了,你用不到的知识在脑中根本不会停留太久。以前看到这个设计模式,我认为这一定是个哪个傻*为了显示自己高级而弄出来没用的东西,我都知道该用哪种实现,都new出来了,还继承个什么劲?结果现实给了我一嘴巴子,只要解决了如何定位哪个子类的使用,那么这种模式对开发而言是极为便利的。我也明白了为什么博客、论坛、技术群里都会说,你不经意用到了设计模式的时候,你才真正懂了设计模式。
4、总结
从事开发到现在也有将近三年的时间,抽象的思维从校验框架启蒙后一直萦绕在我脑海中,一旦遇上相似的业务或者重复的代码,我的第一反应就是能不能抽象。这种思维也确实在我的工作中替我省去了很多麻烦事。抽象,配合反射、注解能够处理很多不必要的重复劳动,让代码真正“智能”起来。但是对于性能有极致需求的话可能不适合用太多反射,毕竟我现在所处的环境对性能要求并没有那么高。
写到这里发现似乎有些啰嗦和跑题,一哥见谅