⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 咖啡拿铁 「谢英豪」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

策略模式

经常在网上看到一些名为“别再if-else走天下了”,“教你干掉if-else”等之类的文章,大部分都会讲到用策略模式去代替if-else。策略模式实现的方式也大同小异。主要是定义统一行为(接口或抽象类),并实现不同策略下的处理逻辑(对应实现类)。客户端使用时自己选择相应的处理类,利用工厂或其他方式。

注解实现

本文要说的是用注解实现策略模式的方式,以及一些注意点。 话不多说,还是以最常 见的订单处理为例。首先定义这样一个订单实体类:

@Data
public class Order {
/**
* 订单来源
*/
private String source;
/**
* 支付方式
*/
private String payMethod;
/**
* 订单编号
*/
private String code;
/**
* 订单金额
*/
private BigDecimal amount;
// ...其他的一些字段
}

假如对于不同来源(pc端、移动端)的订单需要不同的逻辑处理。项目中一般会有OrderService这样一个类,如下,里面有一坨if-else的逻辑,目的是根据订单的来源的做不同的处理。

@Service
public class OrderService {

public void orderService(Order order) {
if(order.getSource().equals("pc")){
// 处理pc端订单的逻辑
}else if(order.getSource().equals("mobile")){
// 处理移动端订单的逻辑
}else {
// 其他逻辑
}
}
}

策略模式就是要干掉上面的一坨if-else,使得代码看起来优雅且高大上。 现在就让我们开始干掉这一坨if-else。先总览下结构:

图片

1.首先定义一个OrderHandler接口,此接口规定了处理订单的方法。

public interface OrderHandler {
void handle(Order order);
}

2.定义一个OrderHandlerType注解,来表示某个类是用来处理何种来源的订单。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface OrderHandlerType {
String source();
}

3.接下来就是实现pc端和移动端订单处理各自的handler,并加上我们所定义的OrderHandlerType注解。

@OrderHandlerType(source = "mobile")
public class MobileOrderHandler implements OrderHandler {
@Override
public void handle(Order order) {
System.out.println("处理移动端订单");
}
}

@OrderHandlerType(source = "pc")
public class PCOrderHandler implements OrderHandler {
@Override
public void handle(Order order) {
System.out.println("处理PC端订单");
}
}

4.以上准备就绪后,就是向spring容器中注入各种订单处理的handler,并在OrderService.orderService方法中,通过策略(订单来源)去决定选择哪一个OrderHandler去处理订单。我们可以这样做:

@Service
public class OrderService {

private Map<String, OrderHandler> orderHandleMap;

@Autowired
public void setOrderHandleMap(List<OrderHandler> orderHandlers) {
// 注入各种类型的订单处理类
orderHandleMap = orderHandlers.stream().collect(
Collectors.toMap(orderHandler -> AnnotationUtils.findAnnotation(orderHandler.getClass(), OrderHandlerType.class).source(),
v -> v, (v1, v2) -> v1));
}

public void orderService(Order order) {
// ...一些前置处理

// 通过订单来源确定对应的handler
OrderHandler orderHandler = orderHandleMap.get(order.getSource());
orderHandler.handle(order);

// ...一些后置处理
}
}

在OrderService中,维护了一个orderHandleMap,它的key为订单来源,value为对应的订单处理器Handler。通过@Autowired去初始化orderHandleMap(这里有一个lambda表达式,仔细看下其实没什么难度的)。这样一来,OrderService.orderService里的一坨if-else不见了,取而代之的仅仅是两行代码。即,先从orderHandleMap中根据订单来源获取对应的OrderHandler,然后执行OrderHandler.handle方法即可。

这种做法的好处是,不论以后业务如何发展致使订单来源种类增加,OrderService的核心逻辑不会改变,我们只需要实现新增来源的OrderHandler即可,且团队中每人开发各自负责的订单来源对应的OrderHandler即可,彼此间互不干扰。

到此,似乎已经讲完了通过注解实现策略模式,干掉if-else的方法,就这样结束了吗?不,真正的重点从现在才开始。

现在回过头看orderHandleMap这个Map,它的key是订单来源,假如,我们想通过订单来源+订单支付方式这两个属性来决定到底使用哪一种OrderHandler怎么办?比如PC端支付宝支付的订单是一种处理逻辑(PCAliPayOrderHandler),PC端微信支付的订单是另外一种处理逻辑(PCWeChatOrderHandler),对应的还有移动端支付宝支付(MobileAliPayOrderHandler)和移动端微信支付(MobileWeChatOrderHandler)。

这时候我们的key应该存什么呢,可能有人会说,我直接存订单来源+订单支付方式组成的字符串不就行了吗?确实可以,但是如果这时业务逻辑又变了(向pm低头),PC端支付宝支付和微信支付是同一种处理逻辑,而移动端支付宝支付和微信支付是不同的处理逻辑,那情况就变成了PCAliPayOrderHandler和PCWeChatOrderHandler这两个类是同一套代码逻辑。我们干掉了if-else,但却造出了两份相同的代码,这是一个作为有强迫症的程序员所不能容忍的。怎么干掉这两个逻辑相同的类呢?

首先,我们可以回顾下,注解它究竟是个什么玩意?不知道大家有没有注意到定义注解的语法,也就是@interface,与定义接口的语法想比,仅仅多了一个@。翻看jdk,可以找到这么一个接口Annotation,如下

/**
* The common interface extended by all annotation types. Note that an
* interface that manually extends this one does <i>not</i> define
* an annotation type. Also note that this interface does not itself
* define an annotation type.
*
* More information about annotation types can be found in section 9.6 of
* <cite>The Java&trade; Language Specification</cite>.
*
* The {@link java.lang.reflect.AnnotatedElement} interface discusses
* compatibility concerns when evolving an annotation type from being
* non-repeatable to being repeatable.
*
* @author Josh Bloch
* @since 1.5
*/
public interface Annotation {
// …省略
}

开头就表明了,The common interface extended by all annotation types。说的很明白了,其实注解它就是个接口,对,它就是个接口而已,@interface仅仅是个语法糖。那么,注解既然是个接口,就必然会有相应的实现类,那实现类哪里来呢?上述中我们仅仅定义了OrderHandlerType注解,别的什么也没有做。这时候不得不提动态代理了,一定是jdk在背后为我们做了些什么。

为了追踪JVM在运行过程中生成的JDK动态代理类。我们可以设置VM启动参数如下:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

该参数可以保存所生成的JDK动态代理类到本地。额外说一句,若我们想追踪cglib所生成的代理类,即对应的字节码文件,可以设置参数:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "保存的路径");

添加参数后再次启动项目,可以看到我们项目里多了许多class文件(这里只截取了部分,笔者启动的项目共生成了97个动态代理类。由于我的项目是springboot环境,因此生成了许多如ConditionalOnMissingBean、Configuration、Autowired等注解对应的代理类):

图片

那接下来就是要找到我们所关心的,即jdk为我们自定义的OrderHandlerType注解所生成的代理类。由于jdk生成的动态代理类都会继承Proxy这个类,而java又是单继承的,所以,我们只需要找到实现了OrderHandlerType的类即可。遍历这些类文件,发现$Proxy63.class实现了我们定义的OrderHandlerType注解:

public final class $Proxy63 extends Proxy implements OrderHandlerType {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m3;
private static Method m0;

public $Proxy63(InvocationHandler var1) throws {
super(var1);
}

// …省略
}

我们知道,jdk动态代理其实现的核心是:

图片

也就是这个构造函数的InvocationHandler对象了。那么注解的InvocationHandler是哪个具体实现呢?不难发现就是:

图片

这个类里面的核心属性,就是那个 memberValues,我们在使用注解时给注解属性的赋值,都存储在这个map里了。而代理类中的各种方法的实现,实际上是调用了 AnnotationInvocationHandler 里的 invoke 方法。

好了,现在我们知道了注解就是个接口,且通过动态代理实现其中所定义的各种方法。那么回到我们的OrderService,为什么不把key的类型设置为OrderHandlerType?就像这样。

private Map<OrderHandlerType, OrderHandler> orderHandleMap;

如此一来,不管决定订单处理器orderhandler的因素怎么变,我们便可以以不变应万变(这不就是我们所追求的代码高扩展性和灵活性么)。那当我们的map的key变成了OrderHandlerType之后,注入和获取的逻辑就要相应改变,注入的地方很好改变,如下:

public class OrderService {

private Map<OrderHandlerType, OrderHandler> orderHandleMap;

@Autowired
public void setOrderHandleMap(List<OrderHandler> orderHandlers) {
// 注入各种类型的订单处理类
orderHandleMap = orderHandlers.stream().collect(
Collectors.toMap(orderHandler -> AnnotationUtils.findAnnotation(orderHandler.getClass(), OrderHandlerType.class),
v -> v, (v1, v2) -> v1));
}
// ...省略
}

那获取的逻辑要怎么实现?我们怎么根据order的来源和支付方式去orderHandleMap里获取对应的OrderHandler呢?问题变成了如何关联order的来源和支付方式与OrderHandlerType注解。

还记得刚才所说的注解就是个接口吗,既然是个接口,我们自己实现一个类不就完事了么,这样就把order的来源和支付方式与OrderHandlerType注解关联起来了。说干就干,现在我们有了这么一个类,

public class OrderHandlerTypeImpl implements OrderHandlerType {

private String source;
private String payMethod;

OrderHandlerTypeImpl(String source, String payMethod) {
this.source = source;
this.payMethod = payMethod;
}

@Override
public String source() {
return source;
}

@Override
public String payMethod() {
return payMethod;
}

@Override
public Class<? extends Annotation> annotationType() {
return OrderHandlerType.class;
}

}

在获取对应OrderHandler时我们可以这样写,

public void orderService(Order order) {
// ...一些前置处理

// 通过订单来源确以及支付方式获取对应的handler
OrderHandlerType orderHandlerType = new OrderHandlerTypeImpl(order.getSource(), order.getPayMethod());
OrderHandler orderHandler = orderHandleMap.get(orderHandlerType);
orderHandler.handle(order);

// ...一些后置处理
}

看起来没什么问题了,来运行一下。不对劲啊,空指针,那个异常它来了。

图片

我们断点打在NPE那一行,

图片

一定是姿势不对,漏掉了什么。那我们就来分析下。orderHandleMap中确实注入了所定义的几个OrderHandler(PCAliPayOrderHandler、PCWeChatOrderHandler、MobileAliPayOrderHandler、MobileWeChatOrderHandler),但是get却没有获取到,这是为什么呢?我们来回忆下,map的get方法逻辑,还记得最开始学习java时的hashCode和equals方法吗?

不知道大家注意到没有,map中key对应的类型是$Proxy63(jdk动态代理给我们生成的),跟我们自己实现的OrderHandlerTypeImpl是不同类型的。 梳理下,在Autowied时,我们放进orderHandleMap的key是动态代理类对象,而获取时,自定义了OrderHandlerTypeI实现类OrderHandlerTypeImpl,而又没有重写hashCode和equals方法,才导致从map中没有获取到我们所想要的OrderHandler,那么,我们把实现类OrderHandlerTypeImpl的hashCode和equals这两个方法重写,保持跟动态代理类生成的一样不就行了吗?

再回看下动态代理给我们生成的这两个方法,前面说了,注解对应的代理类方法调用实际上都是AnnotationInvocationHandler里面的方法,翻看AnnotationInvocationHandler里面的hashCode和equals方法:

private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}

private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}

具体的逻辑也比较简单,就不分析了。那我们就按照AnnotationInvocationHandler中的实现,在我们的OrderHandlerTypeImpl中按照相同的逻辑重写下这两个方法,如下

public class OrderHandlerTypeImpl implements OrderHandlerType {

// …省略

@Override
public int hashCode() {
int hashCode = 0;
hashCode += (127 * "source".hashCode()) ^ source.hashCode();
hashCode += (127 * "payMethod".hashCode()) ^ payMethod.hashCode();
return hashCode;
}


@Override
public boolean equals(Object obj) {
if (!(obj instanceof OrderHandlerType)) {
return false;
}
OrderHandlerType other = (OrderHandlerType) obj;
return source.equals(other.source()) && payMethod.equals(other.payMethod());
}
}

再次运行看看是否达到我们预期,果不其然,这次可以正常获取到了handler,至此,大功告成。

图片

这样以来,不管以后业务怎么发展,OrderService核心逻辑不会改变,只需要扩展OrderHandler即可。

文章目录
  1. 1. 策略模式
  2. 2. 注解实现