通过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 { @Override public double calculateDiscount(double price) { // 普通会员打九折 return price * 0.9; } }
vip会员折扣策略:
@Component("vipDiscount") public class VipDiscountStrategy implements DiscountStrategy { @Override public double calculateDiscount(double price) { // VIP会员打八折 return price * 0.8; } }
使用策略:
@Component public class ShoppingService { @Autowired private 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 { @Autowired private 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 @Async public void handleShippingEvent(OrderStatusChangeEvent event) { Long orderId = event.getOrderId(); System.out.println("订单 " + orderId + " 物流发货"); } } @Component public class AccountingEventListener { @EventListener @Async public void handleAccountingEvent(OrderStatusChangeEvent event) { Long orderId = event.getOrderId(); System.out.println("订单 " + orderId + " 财务记账"); } } @Component public class EDMEventListener { @EventListener @Async public 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 { @Autowired private UserDao userDao; @Transactional(rollbackFor = Exception.class) public void addUser(User user) { userDao.addUser(user); } }
需要注意的是使用声明式事务不当也会让事务失效具体可以看:
2.编程式事务
编程式事务是指通过编写代码来实现事务管理,通常是在Service层中手动开启、提交和回滚事务。虽然这种方式比较繁琐,但是在某些场景下仍然很有用。
例如,我们可以在Service层中使用TransactionTemplate类来实现编程式事务管理:
@Service public class UserService { @Autowired private UserDao userDao; @Autowired private 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 调用可能无法回滚,进而造成数据一致性问题。
不过度使用声明式事务
过多地使用声明式事务,会增加系统的复杂度,使得代码难以理解和维护。
声明式事务可能会引起死锁等性能问题,尤其是在高并发环境下。
对于复杂的事务场景,声明式事务可能无法满足需求,需要使用编程式事务。