Spring高手之路-Spring在业务中常见的使用方式

目录

通过IOC实现策略模式

通过AOP实现拦截增强

1.参数检验

2.缓存逻辑

3.日志记录

通过Event异步解耦

通过Spring管理事务

1.声明式事务

2.编程式事务

3.需要注意的问题

不能在事务中处理分布式缓存

不能在事务中执行 RPC 操作

不过度使用声明式事务


通过IOC实现策略模式

很多时候,我们需要对不同的场景进行不同的业务逻辑处理,举个例子,譬如针对不同类型的用户,购买商品的折扣不同。

普通的逻辑是使用if-else如下:

        //其他逻辑。。。。。。。。。double discount;if(userType==NORMAL){//打九折discount = 0.9;}else if(userType==VIP){//打八折discount = 0.8;}//其他逻辑。。。。。。。。。

随着升级扩展可能会新增用户类型,比如超级会员,打七折。。。。。。。这种if-else逻辑显然不够优雅。

我们可以借助Spring IOC实现策略模式进行优化,只需要将不同的策略类定义成 Spring Bean,然后在需要使用策略的地方通过 IOC 容器获取对应的 Bean 即可。如下步骤

定义折扣策略接口:

public interface DiscountStrategy {double calculateDiscount(double price);
}

普通会员折扣策略:

@Component("normalDiscount")
public class NormalDiscountStrategy implements DiscountStrategy {@Overridepublic double calculateDiscount(double price) {// 普通会员打九折return price * 0.9;}
}

vip会员折扣策略:

@Component("vipDiscount")
public class VipDiscountStrategy implements DiscountStrategy {@Overridepublic double calculateDiscount(double price) {// VIP会员打八折return price * 0.8;}
}

使用策略:

@Component
public class ShoppingService {@Autowiredprivate DiscountStrategy discountStrategy;public double calculateFinalPrice(double price) {// 根据不同的策略计算折扣价double discountPrice = discountStrategy.calculateDiscount(price);// 其他计算逻辑...return discountPrice;}
}

通过AOP实现拦截增强

很多时候,我们一般是通过注解和AOP相结合。大概的实现思路就是先定义一个注解,然后通过AOP去发现使用过该注解的类,对该类的方法进行代理处理,增加额外的逻辑,譬如参数校验,缓存,日志打印等等。

1.参数检验

创建一个自定义的注解@ValidParams来标记需要进行参数校验的方法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidParams {
}

创建一个切面类来拦截带有@ValidParams注解的方法,并在方法执行前进行参数校验


@Aspect
@Component
public class ValidationAspect {@Before("@annotation(com.example.ValidParams)")public void validateParams(JoinPoint joinPoint) {Object[] args = joinPoint.getArgs();// 检查参数是否符合要求for (Object arg : args) {if (arg == null || !isValid(arg)) {throw new IllegalArgumentException("Invalid parameter");}}}private boolean isValid(Object arg) {// 在这里实现具体的参数校验逻辑// 返回true表示参数有效,返回false表示参数无效// 可根据实际需求进行定制化的参数校验逻辑// 这里只是一个示例,实际使用时需要根据具体情况进行修改return arg != null;}
}

2.缓存逻辑

创建自定义缓存注解@@CacheableRedis

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheableRedis {String key();int expireTime() default 3600;
}

创建一个切面类,用于拦截带有@CacheableRedis注解的方法,并实现缓存逻辑

@Aspect
@Component
public class CacheAspect {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Before("@annotation(cacheableRedis)")public Object cache(JoinPoint joinPoint, CacheableRedis cacheableRedis) {String key = cacheableRedis.key();int expireTime = cacheableRedis.expireTime();Object result = redisTemplate.opsForValue().get(key);//缓存中有数据直接返回if (result != null) {return result;}//没有就访问数据库获取,存到缓存里面result = joinPoint.proceed();redisTemplate.opsForValue().set(key, result, expireTime, TimeUnit.SECONDS);return result;}
}

3.日志记录

@Aspect
@Component
public class LogAspect {@Pointcut("execution(* com.example.service.*Service.*(..))")public void servicePointcut() {}@Before("servicePointcut()")public void logBefore(JoinPoint joinPoint) {System.out.println("执行 " + joinPoint.getSignature().getName() + " 方法前记录日志");}@AfterReturning("servicePointcut()")public void logAfterReturning(JoinPoint joinPoint) {System.out.println("执行 " + joinPoint.getSignature().getName() + " 方法后记录日志");}@AfterThrowing(value = "servicePointcut()", throwing = "ex")public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {System.out.println("执行 " + joinPoint.getSignature().getName() + " 方法发生异常,异常信息为:" + ex.getMessage());}
}@Service
public class UserService {public void addUser(User user) {// 这里是添加用户的逻辑}
}

在上面的示例中,我们定义了一个名为 LogAspect 的切面类,并通过 @Pointcut 注解定义了一个切点,表示需要拦截的方法。在 LogAspect 中,我们使用 @Before@AfterReturning@AfterThrowing 注解分别定义了在方法执行前、执行后和发生异常时需要执行的增强方法。在 UserService 中,我们调用了 addUser() 方法,该方法将会被 LogAspect 中的增强方法拦截。

通过Event异步解耦

很多时候,可以一个单据状态的改变,要触发很多下游的行为,举个例子:

订单从确认订单变为支付成功,就要触发物流的发货,财务的记账,EDM触达(通过电子直邮(Electronic Direct Mail)的方式向目标受众发送信息。)等等。但是如果订单状态改变同步触发下游的动作,这样对订单业务非常不友好,下游的每次变动都需要上游感知。所以,对于这种情况,我们就需要Event异步解藕。

首先,定义订单状态改变事件类:

public class OrderStatusChangeEvent extends ApplicationEvent {private Long orderId;private String newStatus;public OrderStatusChangeEvent(Object source, Long orderId, String newStatus) {super(source);this.orderId = orderId;this.newStatus = newStatus;}// 省略getter/setter方法
}

然后,创建一个事件发布者:

@Component
public class OrderEventPublisher {private final ApplicationEventPublisher eventPublisher;public OrderEventPublisher(ApplicationEventPublisher eventPublisher) {this.eventPublisher = eventPublisher;}public void publishOrderStatusChangeEvent(Long orderId, String newStatus) {OrderStatusChangeEvent event = new OrderStatusChangeEvent(this, orderId, newStatus);eventPublisher.publishEvent(event);}
}

接下来,定义物流发货、财务记账和EDM触达的事件监听器:

@Component
public class ShippingEventListener {@EventListener@Asyncpublic void handleShippingEvent(OrderStatusChangeEvent event) {Long orderId = event.getOrderId();System.out.println("订单 " + orderId + " 物流发货");}
}@Component
public class AccountingEventListener {@EventListener@Asyncpublic void handleAccountingEvent(OrderStatusChangeEvent event) {Long orderId = event.getOrderId();System.out.println("订单 " + orderId + " 财务记账");}
}@Component
public class EDMEventListener {@EventListener@Asyncpublic void handleEDMEvent(OrderStatusChangeEvent event) {Long orderId = event.getOrderId();System.out.println("订单 " + orderId + " EDM触达");}
}

最后,在需要改变订单状态的地方,注入事件发布者并触发订单状态改变事件:

@Service
public class OrderService {private final OrderEventPublisher eventPublisher;public OrderService(OrderEventPublisher eventPublisher) {this.eventPublisher = eventPublisher;}public void changeOrderStatus(Long orderId, String newStatus) {// 执行订单状态改变逻辑// 发布订单状态改变事件eventPublisher.publishOrderStatusChangeEvent(orderId, newStatus);}
}

通过Spring管理事务

Spring的事务抽象了下游不同DataSource的实现 (如,JDBC,Mybatis,Hibernate等),让我们不用再关心下游的事务提供方究竟是谁,直接启动事务即可。

1.声明式事务

声明式事务是指在方法或类级别上添加@Transactional注解来实现事务管理。这种方式需要使用Spring的AOP机制来实现,在方法调用前后自动开启和提交事务,同时还能够处理事务回滚等异常情况。

例如,我们可以在Service层中添加@Transactional注解来实现事务管理:

@Service
public class UserService {@Autowiredprivate UserDao userDao;@Transactional(rollbackFor = Exception.class)public void addUser(User user) {userDao.addUser(user);}
}

需要注意的是使用声明式事务不当也会让事务失效具体可以看:

Spring高手之路-Spring事务失效的场景详解-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_62262918/article/details/135614523?spm=1001.2014.3001.5502

2.编程式事务

编程式事务是指通过编写代码来实现事务管理,通常是在Service层中手动开启、提交和回滚事务。虽然这种方式比较繁琐,但是在某些场景下仍然很有用。

例如,我们可以在Service层中使用TransactionTemplate类来实现编程式事务管理:

@Service
public class UserService {@Autowiredprivate UserDao userDao;@Autowiredprivate TransactionTemplate transactionTemplate;public void addUser(final User user) {transactionTemplate.execute(new TransactionCallback<Void>() {public Void doInTransaction(TransactionStatus status) {try {userDao.addUser(user);} catch (Exception e) {status.setRollbackOnly();throw e;}return null;}});}
}

3.需要注意的问题

不能在事务中处理分布式缓存

如果在事务中进行了缓存操作,但事务最终被回滚了,那么缓存中就可能存在脏数据,进而影响业务逻辑。

不能在事务中执行 RPC 操作

在事务中执行 RPC 操作,会增加事务的执行时间,尤其是当 RPC 服务不可用或响应很慢时,会导致事务长时间占用资源,进而影响系统性能和稳定性。此外,在需要回滚事务时,RPC 调用可能无法回滚,进而造成数据一致性问题。

不过度使用声明式事务

过多地使用声明式事务,会增加系统的复杂度,使得代码难以理解和维护。

声明式事务可能会引起死锁等性能问题,尤其是在高并发环境下。

对于复杂的事务场景,声明式事务可能无法满足需求,需要使用编程式事务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/410503.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++初入(四)

1.万能头文件 #include <bits/stdc.h> 里面包含了大量我们日常所需的头文件&#xff0c;如果使用它&#xff0c;我们就可以减少大量时间去写头文件&#xff0c;但是其实在平常练习和实际运用中&#xff0c;该头文件几乎没有实际价值&#xff0c;原因&#xff1a;1.里面…

SpringBoot教程(七) | SpringBoot解决跨域问题

SpringBoot教程(七) | SpringBoot解决跨域问题 上篇文章我们介绍了SpringBoot的拦截器的写法&#xff0c;其中有一个比较重要的步骤&#xff0c;就是把我们写好的拦截器注册到Spring的一个配置类中&#xff0c;这个类是实现了WebMvcConfigurer 接口&#xff0c;这个类很重要&a…

翻译: Streamlit从入门到精通 高级用法缓存Cache和Session 五

Streamlit从入门到精通 系列&#xff1a; 翻译: Streamlit从入门到精通 基础控件 一翻译: Streamlit从入门到精通 显示图表Graphs 地图Map 主题Themes 二翻译: Streamlit从入门到精通 构建一个机器学习应用程序 三翻译: Streamlit从入门到精通 部署一个机器学习应用程序 四 …

Pytorch基础知识点复习

文章目录 并行计算单卡训练多卡训练单机多卡DP多机多卡DDPDP 与 DDP 的优缺点 PyTorch的主要组成模块Pytorch的主要组成模块包括那些呢&#xff1f;Dataset和DataLoader的作用是什么&#xff0c;我们如何构建自己的Dataset和DataLoader&#xff1f;神经网络的一般构造方法&…

.NET国产化改造探索(三)、银河麒麟安装.NET 8环境

随着时代的发展以及近年来信创工作和…废话就不多说了&#xff0c;这个系列就是为.NET遇到国产化需求的一个闭坑系列。接下来&#xff0c;看操作。 上一篇介绍了如何在银河麒麟操作系统上安装人大金仓数据库&#xff0c;这篇文章详细介绍下在银河麒麟操作系统上安装.NET8环境。…

Ubuntu 22.04 安装prometheus

服务器监控和报警软件有很多&#xff0c;为什么我们会选择Prometheus而不是其他软件呢&#xff1f; 因为它有以下优点&#xff1a; 自带简易web监控页面&#xff0c;用户可以很方便地查看监控数据和使用仪表盘。能实时收集数据并根据自定义警报规则推送告警&#xff1b;具有丰…

AI绘图制作红包封面教程

注意&#xff1a;有不懂的话可加入QQ群聊一起交流&#xff1a;901944946欢迎大家关注微信公众号【程序猿代码之路】&#xff0c;每天都会不定时的发送一些红包封面&#xff01;&#xff01; 2024的春节即将到来&#xff0c;而在这春节到来之前&#xff0c;就有一个非常爆火的小…

spring boot学习第八篇:通过spring boot、jedis实现秒单

参考&#xff1a;Redis实现分布式锁的7种方案 - 知乎 1、 准备数据库表&#xff0c;如下SQL表示库存表&#xff0c;有主键ID和库存数量字段 CREATE TABLE t_stock (id bigint(20) NOT NULL AUTO_INCREMENT,quantity bigint(20) NOT NULL,PRIMARY KEY (id) ) ENGINEInnoDB DEF…

0间隔24h采集线报+源码的资源网

一款网站程序零间隔24h采集线报源码的资源网&#xff0c;更新下载类目的采集 及 导入&#xff0c;这款网站程序&#xff1a;jizhiCMS 高仿新版某刀资源网模板进行自动采集。 安装方法&#xff1a; 将根目录文件上传服务器 将根目录文件的sql.sql导入mysql数据库 环境需要支…

【JVM】常用命令

一、前言 Java虚拟机&#xff08;JVM&#xff09;是Java程序运行的基础设施&#xff0c;它负责将Java字节码转换为本地机器代码并执行。在开发过程中&#xff0c;我们经常需要使用一些命令来监控和管理JVM的性能和状态。本文将详细介绍6个常用的JVM命令&#xff1a;jps、jstat…

Linux 系统编程:文件系统的底层逻辑 - inode

《Linux 程序设计》的第三章讲文件操作。在提到 目录 时有这么一段文字&#xff1a; 文件&#xff0c;除了本身包含的 内容 以外&#xff0c;它还会有一个 名字 和一些 属性&#xff0c;即“管理信息”&#xff0c;包括文件的创建 / 修改日期和它的访问权限。这些属性被保存在文…

pytorch一致数据增强—独用增强

前作 [1] 介绍了一种用 pytorch 模仿 MONAI 实现多幅图&#xff08;如&#xff1a;image 与 label&#xff09;同用 random seed 保证一致变换的写法&#xff0c;核心是 MultiCompose 类和 to_multi 包装函数。不过 [1] 没考虑各图用不同 augmentation 的情况&#xff0c;如&am…