使用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 事件。

@Servicepublicclass UserService implements ApplicationEventPublisherAware { // <1>    private Logger logger = LoggerFactory.getLogger(getClass());    private ApplicationEventPublisher applicationEventPublisher;    @Override    public 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 事件,进行自定义处理。

 
@Servicepublicclass 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。这是另一种使用方式。

@Servicepublicclass 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的鉴权逻辑我们假设是这样的:/** * 冷启动权限校验处理器 */@Componentpublicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler {    @Override    public boolean isMatched(BizType bizType) {        return BizType.COLD_START.equals(bizType);    }    @Override    public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) {        //业务特有鉴权逻辑    }}业务2的鉴权逻辑我们假设是这样的:/** * 趋势业务权限校验处理器 */@Componentpublicclass TrendPermissionCheckHandlerImpl implements PermissionCheckHandler {    @Override    public boolean isMatched(BizType bizType) {        return BizType.TREND.equals(bizType);    }    @Override    public 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@Servicepublicclass PermissionServiceImpl    implements PermissionService, ApplicationContextAware, InitializingBean {    private ApplicationContext applicationContext;    //注:这里可以使用Map,偷个懒    private List<PermissionCheckHandler> handlers = new ArrayList<>();    @Override    public 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;    }    @Override    public void afterPropertiesSet() throws Exception {        for (PermissionCheckHandler handler : applicationContext.getBeansOfType(PermissionCheckHandler.class)            .values()) {            handlers.add(handler);            log.warn("load permission check handler [{}]", handler.getClass().getName());        }    }    @Override    public 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等库提供的一些类和规范进行了抽象封装,易用性更好,更贴合开发者需求

摘自https://mp.weixin.qq.com/s/94oe5c_7ouE1GbyiPfNg5g

淘系工程师讲解的使用Spring特性优雅书写业务代码的更多相关文章

  1. myeclipse 去掉spring特性支持

    myeclipse10.0 去掉spring支持  手工修改工程目录下的.project文件中相关的内容 删除<nature>com.genuitec.eclipse.springfram ...

  2. ? 原创: 铲子哥 搜狗测试 今天 shell编程的时候,往往不会把所有功能都写在一个脚本中,这样不太好维护,需要多个脚本文件协同工作。那么问题来了,在一个脚本中怎么调用其他的脚本呢?有三种方式,分别是fork、source和exec。 1. fork 即通过sh 脚本名进行执行脚本的方式。下面通过一个简单的例子来讲解下它的特性。 创建father.sh,内容如下: #!/bin/bas

    ? 原创: 铲子哥 搜狗测试 今天 shell编程的时候,往往不会把所有功能都写在一个脚本中,这样不太好维护,需要多个脚本文件协同工作.那么问题来了,在一个脚本中怎么调用其他的脚本呢?有三种方式,分别 ...

  3. 使用Spring Validation优雅地校验参数

    写得好的没我写得全,写得全的没我写得好 引言 不知道大家平时的业务开发过程中 controller 层的参数校验都是怎么写的?是否也存在下面这样的直接判断? public String add(Use ...

  4. Spring进阶—如何用Java代码实现邮件发送(一)

    相关文章: <Spring进阶—如何用Java代码实现邮件发送(二)> 在一些项目里面如进销存系统,对一些库存不足发出预警提示消息,招聘网站注册用户验证email地址等都需要用到邮件发送技 ...

  5. 用好spring mvc validator可以简化代码

    表单的数据检验对一个程序来讲非常重要,因为对于客户端的数据不能完全信任,常规的检验类型有: 参数为空,根据不同的业务规定要求表单项是必填项 参数值的有效性,比如产品的价格,一定不能是负数 多个表单项组 ...

  6. Spring框架的反序列化远程代码执行漏洞分析(转)

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

  7. JAVA 7新特性——在单个catch代码块中捕获多个异常,以及用升级版的类型检查重新抛出异常

    在Java 7中,catch代码块得到了升级,用以在单个catch块中处理多个异常.如果你要捕获多个异常并且它们包含相似的代码,使用这一特性将会减少代码重复度.下面用一个例子来理解. Java 7之前 ...

  8. spring cloud 优雅停机

    spring cloud 优雅停机 大部分部署项目如果要停掉项目一般都是用kill -9 来杀进程 但是由于Eureka采用心跳的机制来上下线服务,会导致服务消费者调用已经kill的服务提供者然后出错 ...

  9. 从spring源码汲取营养:模仿spring事件发布机制,解耦业务代码

    前言 最近在项目中做了一项优化,对业务代码进行解耦.我们部门做的是警用系统,通俗的说,可理解为110报警.一条警情,会先后经过接警员.处警调度员.一线警员,警情是需要记录每一步的日志,是要可追溯的,比 ...

随机推荐

  1. MAMP的使用

    MAMP下载并安装 下载地址:https://pan.baidu.com/s/1TgoKBG3F59NGO8lEj9mf4Q 密码:2m3d 安装:按照提示,一直下一步直到完成 MAMP操作

  2. SpringBoot内嵌ftp服务

    引入依赖 <!-- https://mvnrepository.com/artifact/org.apache.ftpserver/ftpserver-core --> <depen ...

  3. _MSC_VER值对应的Visual Studio版本

    移步官网查看更多定义 1. 关于 今天使用cmake需要判断_MSC_VER的值是多少,额,官网查了下,还真不少 2. 查看 用下面的代码可以输出宏_MSC_VER的值 #pragma once #i ...

  4. 【LeetCode】141. Linked List Cycle 解题报告(Java & Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 双指针 保存已经走过的路径 日期 [LeetCode ...

  5. 【LeetCode】459. Repeated Substring Pattern 解题报告(Java & Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历子串 日期 [LeetCode] 题目地址:ht ...

  6. 【LeetCode】150. Evaluate Reverse Polish Notation 解题报告(Python)

    [LeetCode]150. Evaluate Reverse Polish Notation 解题报告(Python) 标签: LeetCode 题目地址:https://leetcode.com/ ...

  7. 面试造火箭系列,栽在了cglib和jdk动态代理

    "喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗".声音是从电话那头传来的 "是的,你好".小帅暗喜,大厂终于找上我了. "下面我们来进行一 ...

  8. Azure Data Lake(一) 在NET Core 控制台中操作 Data Lake Storage

    一,引言 Azure Data Lake Storage Gen2 是一组专用于大数据分析的功能,基于 Azure Blob Storage 构建的.Data Lake Storage Gen2 包含 ...

  9. CapstoneCS5212替代RTD2166|DP转VGA转换电路设计方法|CS5212替代方案

    Capstone CS5212适用于设计DP转VGA转换电路,主要用在嵌入式单片机基于工业机或者INTEL X86主板上面,也适用于多个电子配件市场和显示器应用程序,如笔记本电脑.主板.台式机.适配器 ...

  10. HTML5 +Java基础 大一结业认证考试试题 - 云南农业职业技术学院 - 互联网技术学院 - 美和易思校企合作专业

     第1题 [单选题][0.33分][概念理解] 关于java中的逻辑运算符,下列说法正确的是 逻辑运算符||.&&.!都是用于连接两个关系表达式</p> 当&&am ...