使用Spring特性优雅书写业务代码

简介: 大家在日常业务开发工作中相信多多少少遇到过下面这样的几个场景:当某一个特定事件或动作发生以后,需要执行很多联动动作,如果串行去执行的话太耗时,如果引入消息中间件的话又太重了;想要针对不同的传参执行不同的策略,也就是我们常说的策略模式,但10个人可能有10种不同的写法,夹杂在一起总感觉不那么优雅;自己的系统想要调用其他系统提供的能力,但其他系统总是偶尔给你一点“小惊喜”,可能因网络问题报超时异常或被调用的某一台分布式应用机器突然宕机,我们想要优雅无侵入式地引入重试机制。其实上面提到的几个典型业务开发场景Spring都为我们提供了很好的特性支持,我们只需要引入Spring相关依赖就可以

大家在日常业务开发工作中相信多多少少遇到过下面这样的几个场景:

当某一个特定事件或动作发生以后,需要执行很多联动动作,如果串行去执行的话太耗时,如果引入消息中间件的话又太重了;
想要针对不同的传参执行不同的策略,也就是我们常说的策略模式,但10个人可能有10种不同的写法,夹杂在一起总感觉不那么优雅;
自己的系统想要调用其他系统提供的能力,但其他系统总是偶尔给你一点“小惊喜”,可能因网络问题报超时异常或被调用的某一台分布式应用机器突然宕机,我们想要优雅无侵入式地引入重试机制。

其实上面提到的几个典型业务开发场景Spring都为我们提供了很好的特性支持,我们只需要引入Spring相关依赖就可以方便快速的在业务代码当中使用啦,而不用引入过多的三方依赖包或自己重复造轮子。下面我们就来看看Spring提供的强大魔力吧。

使用Spring优雅实现观察者模式

观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,其主要解决一个对象状态改变给其他关联对象通知的问题,保证易用和低耦合。一个典型的应用场景是:当用户注册以后,需要给用户发送邮件,发送优惠券等操作,如下图所示
image.png
使用观察者模式后:
image.png
UserService 在完成自身的用户注册逻辑之后,仅仅只需要发布一个 UserRegisterEvent 事件,而无需关注其它拓展逻辑。其它 Service 可以自己订阅 UserRegisterEvent 事件,实现自定义的拓展逻辑。Spring的事件机制主要由3个部分组成。

  1. ApplicationEvent:通过继承它,实现自定义事件。另外,通过它的 source 属性可以获取事件源,timestamp 属性可以获得发生时间。
  2. ApplicationEventPublisher:通过实现它,来发布变更事件。

3. 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;
@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 事件,进行自定义处理。

@Service
publicclass EmailService implements ApplicationListener { // <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 这个中介,进一步解耦。图示如下,可以看出,观察者模式更加轻量,通常用于单机,而发布订阅模式相对而言更重一些,通常用于分布式环境下的消息通知场景。

image.png

使用Spring Retry优雅引入重试机制

如今,Spring Retry是一个独立的包了(早期是Spring Batch的一部分),下面是使用Spring Retry框架进行重试的几个重要步骤。第一步:加入Spring Retry依赖包

<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.1.2.RELEASE</version>

第二步:在应用中包含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:随机重试等待时间(一般不用)

image.png

Spring Retry的优点很明显,第一,属于Spring大生态,使用起来不会太生硬;第二,只需要在需要重试的方法上加上注解并配置重试策略属性就好,不需要太多侵入代码。

但同时也存在两个主要不足,第一,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效;第二,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验判断重试。如果想要更灵活的重试策略可以考虑使用Guava Retry,也是一个不错的选择。

优雅使用Spring特性完成业务策略模式

策略模式相信大家都应该比较熟悉,它定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。

其适用的场景是这样的:一个大功能,它有许多不同类型的实现(策略类),具体根据客户端来决定采用哪一个策略类。比如下单优惠策略、物流对接策略等,应用场景还是非常多的。

image.png

举一个简单的例子,业务背景是这样的:平台需要根据不同的业务进行鉴权,每个业务的鉴权逻辑不一样,都有自己的一套独立的判断逻辑,因此需要根据传入的 bizType 进行鉴权操作,首先我们定义一个权限校验处理器接口如下。

/**

  • 业务权限校验处理器

*/
publicinterface PermissionCheckHandler {

/**
 * 判断是否是自己能够处理的权限校验类型
 */
boolean isMatched(BizType bizType);
/**
 * 权限校验逻辑
 */
PermissionCheckResultDTO permissionCheck(Long userId, String bizCode);

}
业务1的鉴权逻辑我们假设是这样的:
/**

  • 冷启动权限校验处理器

*/
@Component
publicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler {

@Override
public boolean isMatched(BizType bizType) {
    return BizType.COLD_START.equals(bizType);
}
@Override
public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) {
    //业务特有鉴权逻辑
}

}
业务2的鉴权逻辑我们假设是这样的:
/**

  • 趋势业务权限校验处理器

*/
@Component
publicclass 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 属性执行定制初始化逻辑。

image.png

可以很明显的看出,如果以后还有一些其他的业务需要制定相应的鉴权逻辑,我们只需要编写对应的策略类就好了,无需再破坏当前 Service 类的逻辑,很好的保证了开闭原则。

/**

  • 权限校验服务类

*/
@Slf4j
@Service
publicclass 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等库提供的一些类和规范进行了抽象封装,易用性更好,更贴合开发者需求。

相关文章
|
2月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
284 3
|
1月前
|
人工智能 监控 Java
零代码改造 + 全链路追踪!Spring AI 最新可观测性详细解读
Spring AI Alibaba 通过集成 OpenTelemetry 实现可观测性,支持框架原生和无侵入探针两种方式。原生方案依赖 Micrometer 自动埋点,适用于快速接入;无侵入探针基于 LoongSuite 商业版,无需修改代码即可采集标准 OTLP 数据,解决了原生方案扩展性差、调用链易断链等问题。未来将开源无侵入探针方案,整合至 AgentScope Studio,并进一步增强多 Agent 场景下的观测能力。
1395 33
|
1月前
|
安全 Java 测试技术
《深入理解Spring》单元测试——高质量代码的守护神
Spring测试框架提供全面的单元与集成测试支持,通过`@SpringBootTest`、`@WebMvcTest`等注解实现分层测试,结合Mockito、Testcontainers和Jacoco,保障代码质量,提升开发效率与系统稳定性。
|
2月前
|
安全 IDE Java
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
本文介绍了如何在 Spring 应用程序中使用 Project Lombok 的 `@Data` 和 `@FieldDefaults` 注解来减少样板代码,提升代码可读性和可维护性,并探讨了其适用场景与限制。
138 0
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
|
9月前
|
负载均衡 Java API
Spring Cloud是什么及基本特性都有哪些?
Spring Cloud 是用于构建健壮云应用的框架,包含多个子项目。其核心组件如Eureka(服务注册与发现)、Hystrix(熔断器)、Ribbon(负载均衡)等,帮助开发者快速实现微服务架构。Spring Cloud 提供了服务注册与发现、分布式配置、路由、断路器等功能,简化了微服务开发与管理。本文将重点介绍服务注册与发现及分布式配置两大特性。
497 5
|
5月前
|
Java 数据库连接 数据库
Spring boot 使用mybatis generator 自动生成代码插件
本文介绍了在Spring Boot项目中使用MyBatis Generator插件自动生成代码的详细步骤。首先创建一个新的Spring Boot项目,接着引入MyBatis Generator插件并配置`pom.xml`文件。然后删除默认的`application.properties`文件,创建`application.yml`进行相关配置,如设置Mapper路径和实体类包名。重点在于配置`generatorConfig.xml`文件,包括数据库驱动、连接信息、生成模型、映射文件及DAO的包名和位置。最后通过IDE配置运行插件生成代码,并在主类添加`@MapperScan`注解完成整合
1022 1
Spring boot 使用mybatis generator 自动生成代码插件
|
4月前
|
安全 Java Nacos
0代码改动实现Spring应用数据库帐密自动轮转
Nacos作为国内被广泛使用的配置中心,已经成为应用侧的基础设施产品,近年来安全问题被更多关注,这是中国国内软件行业逐渐迈向成熟的标志,也是必经之路,Nacos提供配置加密存储-运行时轮转的核心安全能力,将在应用安全领域承担更多职责。
|
4月前
|
Java 数据库连接 API
Java 8 + 特性及 Spring Boot 与 Hibernate 等最新技术的实操内容详解
本内容涵盖Java 8+核心语法、Spring Boot与Hibernate实操,按考试考点分类整理,含技术详解与代码示例,助力掌握最新Java技术与应用。
155 2
|
5月前
|
Java 调度 流计算
基于Java 17 + Spring Boot 3.2 + Flink 1.18的智慧实验室管理系统核心代码
这是一套基于Java 17、Spring Boot 3.2和Flink 1.18开发的智慧实验室管理系统核心代码。系统涵盖多协议设备接入(支持OPC UA、MQTT等12种工业协议)、实时异常检测(Flink流处理引擎实现设备状态监控)、强化学习调度(Q-Learning算法优化资源分配)、三维可视化(JavaFX与WebGL渲染实验室空间)、微服务架构(Spring Cloud构建分布式体系)及数据湖建设(Spark构建实验室数据仓库)。实际应用中,该系统显著提升了设备调度效率(响应时间从46分钟降至9秒)、设备利用率(从41%提升至89%),并大幅减少实验准备时间和维护成本。
343 0

热门文章

最新文章