使用Spring特性优雅书写业务代码
作者:阿里巴巴淘系技术
链接:https://www.zhihu.com/question/60761181/answer/1737592739
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
分享一套使用Spring特性优雅书写业务代码的方法。
大家在日常业务开发工作中相信多多少少遇到过下面这样的几个场景:
- 当某一个特定事件或动作发生以后,需要执行很多联动动作,如果串行去执行的话太耗时,如果引入消息中间件的话又太重了;
- 想要针对不同的传参执行不同的策略,也就是我们常说的策略模式,但10个人可能有10种不同的写法,夹杂在一起总感觉不那么优雅;
- 自己的系统想要调用其他系统提供的能力,但其他系统总是偶尔给你一点“小惊喜”,可能因网络问题报超时异常或被调用的某一台分布式应用机器突然宕机,我们想要优雅无侵入式地引入重试机制。
其实上面提到的几个典型业务开发场景Spring都为我们提供了很好的特性支持,我们只需要引入Spring相关依赖就可以方便快速的在业务代码当中使用啦,而不用引入过多的三方依赖包或自己重复造轮子。下面我们就来看看Spring提供的强大魔力吧。
使用Spring优雅实现观察者模式
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,其主要解决一个对象状态改变给其他关联对象通知的问题,保证易用和低耦合。一个典型的应用场景是:当用户注册以后,需要给用户发送邮件,发送优惠券等操作,如下图所示。
使用观察者模式后:
UserService 在完成自身的用户注册逻辑之后,仅仅只需要发布一个 UserRegisterEvent 事件,而无需关注其它拓展逻辑。其它 Service 可以自己订阅 UserRegisterEvent 事件,实现自定义的拓展逻辑。Spring的事件机制主要由3个部分组成。
- ApplicationEvent:通过继承它,实现自定义事件。另外,通过它的 source 属性可以获取事件源,timestamp 属性可以获得发生时间。
- ApplicationEventPublisher:通过实现它,来发布变更事件。
- ApplicationEventListener:通过实现它,来监听指定类型事件并响应动作。这里就以上面的用户注册为例,来看看代码示例。首先定义用户注册事件 UserRegisterEvent。
publicclass UserRegisterEvent extends ApplicationEvent {/*** 用户名*/private String username;public UserRegisterEvent(Object source) {super(source);}public UserRegisterEvent(Object source, String username) {super(source);this.username = username;}public String getUsername() {return username;}
}
然后定义用户注册服务类,实现 ApplicationEventPublisherAware 接口,从而将 ApplicationEventPublisher 注入进来。从下面代码可以看到,在执行完注册逻辑后,调用了 ApplicationEventPublisher的 publishEvent(ApplicationEvent event) 方法,发布了 UserRegisterEvent 事件。
@Service
publicclass UserService implements ApplicationEventPublisherAware { // <1>private Logger logger = LoggerFactory.getLogger(getClass());private ApplicationEventPublisher applicationEventPublisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;}public void register(String username) {// ... 执行注册逻辑logger.info("[register][执行用户({}) 的注册逻辑]", username);// <2> ... 发布applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username));}
}
创建邮箱Service,实现 ApplicationListener 接口,通过 E 泛型设置感兴趣的事件,实现 onApplicationEvent(E event) 方法,针对监听的 UserRegisterEvent 事件,进行自定义处理。
@Service
publicclass EmailService implements ApplicationListener<UserRegisterEvent> { // <1>private Logger logger = LoggerFactory.getLogger(getClass());@Override@Async// <3>public void onApplicationEvent(UserRegisterEvent event) { // <2>logger.info("[onApplicationEvent][给用户({}) 发送邮件]", event.getUsername());}
}
创建优惠券Service,不同于上面的实现 ApplicationListener 接口方式,在方法上,添加 @EventListener 注解,并设置监听的事件为 UserRegisterEvent。这是另一种使用方式。
@Service
publicclass CouponService {private Logger logger = LoggerFactory.getLogger(getClass());@EventListener// <1>public void addCoupon(UserRegisterEvent event) {logger.info("[addCoupon][给用户({}) 发放优惠劵]", event.getUsername());}
}
看到这里,细心的同学可能想到了发布订阅模式,其实观察者模式于发布订阅还是有区别的,简单来说,发布订阅模式属于广义上的观察者模式,在观察者模式的 Subject 和 Observer 的基础上,引入 Event Channel 这个中介,进一步解耦。图示如下,可以看出,观察者模式更加轻量,通常用于单机,而发布订阅模式相对而言更重一些,通常用于分布式环境下的消息通知场景。
使用Spring Retry优雅引入重试机制
如今,Spring Retry是一个独立的包了(早期是Spring Batch的一部分),下面是使用Spring Retry框架进行重试的几个重要步骤。第一步:加入Spring Retry依赖包
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId><version>1.1.2.RELEASE</version>
</dependency>
第二步:在应用中包含main()方法的类或者在包含@Configuration的类上加上@EnableRetry注解 第三步:在想要进行重试的方法(可能发生异常)上加上@Retryable注解
@Retryable(maxAttempts=5,backoff = @Backoff(delay = 3000))
public void retrySomething() throws Exception{logger.info("printSomething{} is called");thrownew SQLException();
}
在上面这个案例当中的重试策略就是重试5次,每次延时3秒。详细的使用文档看这里,它的主要配置参数有下面这样几个。其中exclude、include、maxAttempts、value几个属性很容易理解,比较看不懂的是backoff属性,它也是个注解,包含delay、maxDelay、multiplier、random四个属性。
- delay:如果不设置的话默认是1秒
- maxDelay:最大重试等待时间
- multiplier:用于计算下一个延迟时间的乘数(大于0生效)
- random:随机重试等待时间(一般不用)
Spring Retry的优点很明显,第一,属于Spring大生态,使用起来不会太生硬;第二,只需要在需要重试的方法上加上注解并配置重试策略属性就好,不需要太多侵入代码。
但同时也存在两个主要不足,第一,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效;第二,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验判断重试。如果想要更灵活的重试策略可以考虑使用Guava Retry,也是一个不错的选择。
优雅使用Spring特性完成业务策略模式
策略模式相信大家都应该比较熟悉,它定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。
其适用的场景是这样的:一个大功能,它有许多不同类型的实现(策略类),具体根据客户端来决定采用哪一个策略类。比如下单优惠策略、物流对接策略等,应用场景还是非常多的。
举一个简单的例子,业务背景是这样的:平台需要根据不同的业务进行鉴权,每个业务的鉴权逻辑不一样,都有自己的一套独立的判断逻辑,因此需要根据传入的 bizType 进行鉴权操作,首先我们定义一个权限校验处理器接口如下。
/*** 业务权限校验处理器*/
publicinterface PermissionCheckHandler {/*** 判断是否是自己能够处理的权限校验类型*/boolean isMatched(BizType bizType);/*** 权限校验逻辑*/PermissionCheckResultDTO permissionCheck(Long userId, String bizCode);
}
业务1的鉴权逻辑我们假设是这样的:
/*** 冷启动权限校验处理器*/
@Component
publicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler {@Overridepublic boolean isMatched(BizType bizType) {return BizType.COLD_START.equals(bizType);}@Overridepublic PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) {//业务特有鉴权逻辑}
}
业务2的鉴权逻辑我们假设是这样的:
/*** 趋势业务权限校验处理器*/
@Component
publicclass TrendPermissionCheckHandlerImpl implements PermissionCheckHandler {@Overridepublic boolean isMatched(BizType bizType) {return BizType.TREND.equals(bizType);}@Overridepublic PermissionCheckResultDTO permissionCheck(Long userId, String bizCode){//业务特有鉴权逻辑}
}
可能还有很多其他的业务鉴权逻辑,这里就不一一列举了,实现逻辑像上面这样组织就好了。接着就到了关键的地方了,上面我们定义了这么多策略,应该怎么优雅的组织起来呢,这就需要用到Spring提供的一些扩展特性了,Spring主要为我们提供了三类扩展点,分别对应不同Bean生命周期阶段:
- Aware接口
- BeanPostProcessor
- InitializingBean 和 init-method
我们这里用到的主要是 Aware 接口和 InitializingBean 两个扩展点,其主要用法如下代码所示,关键点就在于实现 ApplicationContextAware 接口的 setApplicationContext 方法和 InitializingBean 接口的 afterPropertiesSet 方法。
实现 ApplicationContextAware 接口的目的就是要拿到 Spring 容器的资源,从而方便的使用它提供的 getBeansOfType 方法(该方法返回的是 map 类型,key 对应 beanName, value 对应 bean);而实现 InitializingBean 接口的目的则是方便为 Service 类的 handlers 属性执行定制初始化逻辑。
可以很明显的看出,如果以后还有一些其他的业务需要制定相应的鉴权逻辑,我们只需要编写对应的策略类就好了,无需再破坏当前 Service 类的逻辑,很好的保证了开闭原则。
/*** 权限校验服务类*/
@Slf4j
@Service
publicclass PermissionServiceImplimplements PermissionService, ApplicationContextAware, InitializingBean {private ApplicationContext applicationContext;//注:这里可以使用Map,偷个懒private List<PermissionCheckHandler> handlers = new ArrayList<>();@Overridepublic PermissionCheckResultDTO permissionCheck(ArtemisSellerBizType artemisSellerBizType, Long userId,String bizCode) {//省略一些前置逻辑PermissionCheckHandler handler = getHandler(artemisSellerBizType);return handler.permissionCheck(userId, bizCode);}private PermissionCheckHandler getHandler(ArtemisSellerBizType artemisSellerBizType) {for (PermissionCheckHandler handler : handlers) {if (handler.isMatched(artemisSellerBizType)) {return handler;}}returnnull;}@Overridepublic void afterPropertiesSet() throws Exception {for (PermissionCheckHandler handler : applicationContext.getBeansOfType(PermissionCheckHandler.class).values()) {handlers.add(handler);log.warn("load permission check handler [{}]", handler.getClass().getName());}}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}
}
当然在这里相信不少同学会有疑问,那就是这里在获取 handler 处理器 bean 的时候,所有的 bean 是不是已经初始化好了?会不会存在有的 handler 还没有初始化好的情况?
答案是不会的,Spring Bean 的声明周期保证了这一点(当然前提是 handler 自身不会有特殊的初始化逻辑)。经过实际验证,所有的 handler 会在 Service 初始化操作前 ready,感兴趣的同学可以编写代码验证,可以先在相应钩子处打上日志直接输出结果验证,然后在 Spring 源码关键处打上断点 debug,相信会有不少收获。
总结&思考
公司里的有些代码有点年龄,有些类写的又臭又长,很多地方充斥着代码坏味道,如重复的代码,过长的参数列,散弹式修改,基本型偏执等等,不一一展开。每天要面对这些代码进行开发,不仅消磨了我们对技术的热情也让人变得毫无斗志,很多同学会想——反正都已经这样了,那我也就这么来吧,相信不少小伙伴都有这样的遭遇与困惑。
但唯一不能停下来的就是进步,即使面对恶龙还是不能放弃抵抗。当然,在做需求的时候,很多时候也不能去修改那些代码,太耗时太费劲,风险太大。那自己起码也要思考一下如何设计代码才能去避免以后出现同样的情况,让自己下次不要犯同样的错误。
当我们在实际编写代码的时候,需要留意探索一下Spring有没有为我们提供一些已有的工具类和扩展点。一方面,使用Spring提供的这些特性可以让我们少造轮子,避免引入其他比较重的类库;另一方面,Spring对JDK等库提供的一些类和规范进行了抽象封装,易用性更好,更贴合开发者需求。
使用Spring特性优雅书写业务代码相关推荐
- 6种更优雅书写Python代码!
1 简介 一些比较熟悉pandas的读者朋友应该经常会使用query().eval().pipe().assign()等pandas的常用方法,书写可读性很高的「链式」数据分析处理代码,从而更加丝滑流 ...
- python代码书写_如何优雅的书写Python代码, python使用小技巧
博客链接 文章目录 Python使用技巧 变量命名技巧 用有意义易读的命名 同类型使用相同词汇 可搜索的名字 自我描述的变量 不要取隐晦的名字 精简不重复 默认参数代替运算和条件 实用小窍门 变量值交 ...
- 为什么spring中的controller跳转出错_你的业务代码中Spring声明式事务处理正确了吗?
Spring 针对 Java Transaction API (JTA).JDBC.Hibernate 和 Java Persistence API(JPA) 等事务 API,实现了一致的编程模型,而 ...
- 参与知乎 live — 编写优雅的前端业务代码总结
知乎 live 原地址:编写优雅的前端业务代码 前言 当我们在写业务代码的时候,我们到底在写什么? 其实是对交互的一些处理.所有的交互都是基于用户或者浏览器的一些行为来触发的,比如渲染页面,在页面on ...
- 使用 Spring Validation 优雅地进行参数校验
引言 不知道大家平时的业务开发过程中 controller 层的参数校验都是怎么写的?是否也存在下面这样的直接判断? public String add(UserVO userVO) {if(user ...
- 五星元老大飞哥,教半年Java实习生小飞飞:优雅解决历史代码中的新需求
事件起因 6月中旬,可爱的产品大飞哥穿着鸿星尔克,帅气极了,过来给我提了一个临时需求,需要我对商品创建/更新业务中由开放平台对接而来的请求做一个Check,如果符合要求,则再做一段稍微复杂的逻辑处理. ...
- 写代码犹如写文章: “大师级程序员把系统当故事来讲,而不是当做程序来写” | 如何架构设计复杂业务系统? 如何写复杂业务代码?
写代码犹如写文章: "大师级程序员把系统当故事来讲,而不是当做程序来写" | 如何架构设计复杂业务系统? 如何写复杂业务代码? Kotlin 开发者社区 "大师级程序员把 ...
- 要写好业务代码,也没这么简单!
欢迎关注方志朋的博客,回复"666"获面试宝典 来源:https://blog.csdn.net/new_com/article/details/108399421 为什么要写好业 ...
- 业务代码解构利器--SWAK
简介 业务的不断发展.商品类型的不断增多.不断添加的业务需求使得闲鱼的代码出现"bad smell"--平台代码和业务代码耦合严重难以分离:业务和业务之间代码交织缺少拆解.这也是行 ...
最新文章
- mysql出现1499错误_连接MySQL时出现1449与1045异常解决办法
- 华为交换机重置命令(reset saved-configuration)
- NTU 课程笔记:CV6422 置信区间
- 蓝桥杯——寻找数组中的最大值
- 百度贴吧发帖软件_贴吧自动发帖软件
- docker搞个wordpress
- 百度又做电商;ofo 退出日本;Kotlin 1.3 正式发布 | 极客头条
- C++11 线程对象创建后既不join()也不detach()的后果
- 如何去除html的flash,去除网站Flash动画效果,提升seo优化友好度
- 机器学习-DBSCAN密度聚类
- linux4.6内核lcd驱动源代码,提供基于linux-2.6.32.2 内核100%完全可以使用的驱动源代码,绝无库文件,敬请放心使用。...
- 苹果Mac安装win10双系统
- Ios android crash report,iOS App 后台 Crash 调查
- 相濡以沫,不如相忘于江湖...
- 对DestoryWindow的解释.
- 厚基础Linux——学习笔记(一)
- 学生管理系统登入页面
- 英特尔迅驰二代风尚盛典刮起酷炫风暴!
- 【源码】iOS指纹解锁Touch ID的开发
- XX系统XX版本 测试报告
热门文章
- 三次握手和四次挥手之间的关系
- Web前端开发笔记——第一章 Web前端概论
- false例句_false是什么意思_false的翻译_音标_读音_用法_例句_爱词霸在线词典
- ae saber插件_入门AE:影视特效中的黑洞是怎么做的?附带教程
- linux fedora14 u盘运行,怎么把fedora 14 装进U盘里
- dataframe两个表合并_R语言读取多个excel文件后合并:rbind/merge/cmd合并
- 如何查询spark版本_掌握Spark SQL中的查询执行
- java委托机制教程_通过反射实现Java下的委托机制代码详解
- a5松下驱动器参数设置表_松下伺服几个参数需要熟悉并掌握设置方法
- android 自定义键盘字体大小,android.inputmethodservice.KeyboardView 自定义键盘 字体大小设置...