设计模式综合运用「终于解决」

(32) 2023-07-25 16:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说设计模式综合运用「终于解决」,希望能够帮助你!!!。

1. 门面+模版方法+责任链+策略

1.1 项目背景

在公司的一个实际项目中,需要做一个第三方公司(以下简称XHF)的系统集成工作,把该公司的一些订单数据集成到自己公司平台下,各个订单具有一些共性,但是也有其特有的特征。 经过设计,目前我把订单分为POLICY和XHF类型(暂且这么说吧,反正就是一种订单类型,大家参照着看就OK)。

在订单数据集成到公司平台前,需要对订单数据进行一些必要的业务逻辑校验操作,并且每个订单都有自己的校验逻辑(包含公共的校验逻辑)。 本节介绍的便是整个订单集成系统中的校验逻辑在综合利用设计模式的基础上进行架构设计。

1.2 校验逻辑

本校验逻辑主要分为四个部分:

  1. 校验文件名称(RequestValidator.validateFileInfo)
  2. 校验文件内容中的概要部分(RequestValidator.validateSummary)
  3. 校验文件内容中的列名称(RequestValidator.validateHeaders)
  4. 校验文件内容中的明细(RequestValidator.validateDetails)

其实上面的RequestValidator的实现逻辑最后都是委托给RequestValidationFacade这个门面类进行相应的校验操作。

1.3 实现细节

1.3.1 domain介绍

主要分为RequestFile和RequestDetail两个domain,RequestFile接收泛型的类型(即RequestFile), 使得其子类能够自动识别相应的RequestDetail的子类。RequestFile为抽象类,定义了以下抽象方法,由子类实现:

//由子类实现具体的获取文件明细内容
public abstract List<T> getRequestDetails();
//由子类实现具体的获取workflow的值
public abstract WorkflowEnum getProcessWorkFlow();
//由子类实现文件列字段名列表
public abstract String[] getDetailHeaders();

RequestDetail及其子类就是workflow对应文件的明细内容。

1.3.2 WorkflowEnum枚举策略

本例中如下规定:

  1. workflow为WorkflowEnum.POLICY对应文件名为:xhf_policy_yyyyMMdd_HHmmss_count.txt
  2. workflow为WorkflowEnum.XHF对应文件名为:xhf_integration_yyyyMMdd_HHmmss_count.txt

以上校验逻辑在AbstractRequestValidation类相应的子类中实现(validateFileName方法),其实这个枚举贯穿整个校验组件,它就是一个针对每个业务流程定义的一个枚举策略。

1.3.3 涉及到的设计模式实现思路

1.3.3.1 门面模式

在客户端调用程序中,采用门面模式进行统一的入口(门面模式讲究的是脱离具体的业务逻辑代码)。门面模式封装的结果就是避免高层模块深入子系统内部,同时提供系统的高内聚、低耦合的特性。

此案例中,门面类为RequestValidationFacade,然后各个门面方法的参数均为抽象类RequestFile,通过RequestFile->getProcessWorkFlow()决定调用AbstractRequestValidation中的哪个子类。 AbstractRequestValidation类构造方法中定义了如下逻辑:

requestValidationHandlerMap.put(this.accessWorkflow(),this.accessBeanName());

把子类中Spring自动注入的实体bean缓存到requestValidationHandlerMap中,key即为WorkflowEnum枚举值,value为spring bean name, 然后在门面类中可以通过对应的枚举值取得BeanName,进而得到AbstractRequestValidation相应的子类对象,进行相应的校验操作。

注:这边动态调用到AbstractRequestValidation相应的子类对象,其实也是隐藏着【策略模式】的影子。

1.3.3.2 模版方法模式

在具体的校验逻辑中,用到核心设计模式便是模版方法模式,AbstractRequestValidation抽象类中定义了以下抽象方法:

    /**
     * validate the file details
     * @param errMsg
     * @param requestFile
     * @return
     */
    protected abstract StringBuilder validateFileDetails(StringBuilder errMsg,RequestFile requestFile);

    /**
     * validate the file name
     * @param fileName
     * @return
     */
    protected abstract String validateFileName(String fileName);

    /**
     * return the current xhf_UPDATE_WORKFLOW.UPDATE_WORKFLOW_ID
     * @return
     */
    protected abstract WorkflowEnum accessWorkflow();

    /**
     * return the current file name's format ,such as: xhf_policy_yyyyMMdd_HHmmss_count.txt
     * @return
     */
    protected abstract String accessFileNameFormat();

    /**
     * return the subclass's spring bean name
     * @return
     */
    protected abstract String accessBeanName();

以上抽象方法就类似我们常说的钩子函数,由子类实现即可。

1.3.3.3 责任链模式

在AbstractRequestValidation抽象类中有个抽象方法validateFileDetails,校验的是文件的明细内容中的相应业务规则,此为核心校验, 较为复杂,而且针对每个业务流程,其校验逻辑相差较大,在此处,利用了责任链模式进行处理。

Validator为校验器的父接口,包含两个泛型参数(即:<R extends RequestDetail,F extends RequestFile>),其实现类可以方便的转换需要校验的文件明细。

String doValidate(R detail, F file, ValidatorChain chain) throws BusinessValidationException;

该方法含有一个ValidatorChain参数,就自然而然的为该校验器形成一个链条提供便利条件。

ValidatorChain为校验器链,含有两个接口方法:

 String doValidate(T requestDetail, F requestFile) throws BusinessValidationException;

 ValidatorChain addValidator(Validator validator, WorkflowEnum workflowId);

该处有一个addValidator方法,为ValidatorChain对象添加校验器的方法,返回本身。对应于每个业务流程需要哪些校验器就在此实现即可(即AbstractRequestValidation的子类方法validateFileDetails)。

1.3.3.4 策略模式

如果单单从上面的校验器实现上来看,如果需要增加一个校验器,就需要在AbstractRequestValidation的子类方法validateFileDetails中添加,然后进行相应的校验操作。这样就会非常的麻烦,没有做到真正的解耦。 此时,策略模式就发挥到了可以动态选择某种校验策略的作用(Validator的实现类就是一个具体的校验策略)。

AbstractValidatorHandler抽象类持有FileDetailValidatorChain类的对象,并且实现累Spring的一个接口ApplicationListener(是为了Spring容器启动完成的时候自动把相应的校验器加入到校验器链中)。 核心就是WorkflowEnum这个策略枚举的作用,在子类可以动态的取得相应的校验器对象。

根据子类提供需要的校验器所在的包名列表和不需要的校验器列表,动态配置出需要的校验器链表。核心实现逻辑如下:

private void addValidators() {
    List<Class<? extends Validator>> validators = getValidators();

    validators.forEach((validator) -> {
        String simpleName = validator.getSimpleName();
        String beanName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);

        LOGGER.info("Added validator:{},spring bean name is:{}",simpleName,beanName);

        Validator validatorInstance = ApplicationUtil.getApplicationContext().getBean(beanName,validator);

        fileDetailValidatorChain.addValidator(validatorInstance,getWorkflowId());

    });
}

具体实现可以参考代码即可。

该类含有以下几个抽象方法:

protected abstract WorkflowEnum getWorkflowId();
/**
 * the package need to be added the validators
 * @return
 */
protected abstract Set<String> getBasePackages();

/**
 * the classes need to be excluded
 * @return
 */
protected abstract Set<Class> excludeClasses();

2. 门面+模版方法+责任链+策略+工厂方法

上一节在实现策略模式的实现上,发现了一个弊端:那就是如果在后续业务发展中,需要再次增加一个业务策略的时候,则需要再次继承AbstractValidatorHandler类(详情请参见上篇文章),这样就会造成一定的类膨胀。今天我利用注解的方式改造成动态策略模式,这样就只需要关注自己的业务类即可,无需再实现一个类似的Handler类。

2.1 项目背景

2.1.1 项目简介

在公司的一个业务系统中,有这样的一个需求,就是根据不同的业务流程,可以根据不同的组合主键策略进行动态的数据业务查询操作。在本文中,我假设有这样两种业务,客户信息查询和订单信息查询,对应以下枚举类:

/**
 * 业务流程枚举
 * @author landyl
 * @create 11:18 AM 05/07/2018
 */
public enum WorkflowEnum {
    ORDER(2),
    CUSTOMER(3),
    ;
    
}

每种业务类型都有自己的组合主键查询规则,并且有自己的查询优先级,比如客户信息查询有以下策略:

  1. customerId
  2. requestId
  3. birthDate+firstName

以上仅是假设性操作,实际业务规则比这复杂的多

2.1.2 流程梳理

主要业务流程,可以参照以下简单的业务流程图。

2.1.2.1 查询抽象模型

设计模式综合运用「终于解决」_https://bianchenghao6.com/blog__第1张

2.1.2.2 组合主键查询策略

设计模式综合运用「终于解决」_https://bianchenghao6.com/blog__第2张

2.1.2.3 组合主键查询责任链

设计模式综合运用「终于解决」_https://bianchenghao6.com/blog__第3张

2.2 Java注解简介

注解的语法比较简单,除了@符号的使用之外,它基本与Java固有语法一致。

2.2.1 元注解

JDK1.5提供了4种标准元注解,专门负责新注解的创建。

注解说明@Target表示该注解可以用于什么地方,可能的ElementType参数有:
CONSTRUCTOR:构造器的声明
FIELD:域声明(包括enum实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口(包括注解类型)或enum声明@Retention表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:
SOURCE:注解将被编译器丢弃
CLASS:注解在class文件中可用,但会被VM丢弃
RUNTIME:JVM将在运行期间保留注解,因此可以通过反射机制读取注解的信息@Document将注解包含在Javadoc中@Inherited允许子类继承父类中的注解

2.2.2 自定义注解

定义一个注解的方式相当简单,如下代码所示:

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
//使用@interface关键字定义注解
public @interface Description {
    /*
     * 注解方法的定义(其实在注解中也可以看做成员变量)有如下的规定:
     * 1.不能有参数和抛出异常
     * 2.方法返回类型只能为八种基本数据类型和字符串,枚举和注解以及这些类型构成的数组
     * 3.可以包含默认值,通过default实现
     * 4.如果只有一个方法(成员变量),最好命名为value
     */
    String value();
    int count() default 1; //默认值为1
}

注解的可用的类型包括以下几种:所有基本类型、String、Class、enum、Annotation、以上类型的数组形式。元素不能有不确定的值,即要么有默认值,要么在使用注解的时候提供元素的值。而且元素不能使用null作为默认值。注解在只有一个元素且该元素的名称是value的情况下,在使用注解的时候可以省略“value=”,直接写需要的值即可。

2.2.3 使用注解

如上所示的注解使用如下:

/**
 * @author landyl
 * @create 2018-01-12:39 PM
 */
//在类上使用定义的Description注解
@Description(value="class annotation",count=2)
public class Person {
    private String name;
    private int age;

    //在方法上使用定义的Description注解
    @Description(value="method annotation",count=3)
    public String speak() {
        return "speaking...";
    }
}

使用注解最主要的部分在于对注解的处理,那么就会涉及到注解处理器。从原理上讲,注解处理器就是通过反射机制获取被检查方法上的注解信息,然后根据注解元素的值进行特定的处理。

/**
 * @author landyl
 * @create 2018-01-12:35 PM
 * 注解解析类
 */
public class ParseAnnotation {
    public static void main(String[] args){
        //使用类加载器加载类
        try {
            Class c = Class.forName("com.annatation.Person");//加载使用了定义注解的类
            //找到类上的注解
            boolean isExist = c.isAnnotationPresent(Description.class);
            if(isExist){
                //拿到注解示例
                Description d = (Description)c.getAnnotation(Description.class);
                System.out.println(d.value());
            }
            //找到方法上的注解
            Method[] ms = c.getMethods();
            for(Method m : ms){
                boolean isMExist = m.isAnnotationPresent(Description.class);
                if(isMExist){
                    Description d = m.getAnnotation(Description.class);
                    System.out.println(d.value());
                }
            }
            //另外一种注解方式
            for(Method m:ms){
                Annotation[] as = m.getAnnotations();
                for(Annotation a:as){
                    if(a instanceof Description){
                        Description d = (Description)a;
                        System.out.println(d.value());
                    }
                }

            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}

2.3 策略模式升级版

2.3.1 策略模式实现方式

  1. 使用工厂进行简单的封装
  2. 使用注解动态配置策略
  3. 使用模版方法模式配置策略(参见1. 门面+模版方法+责任链+策略)
  4. 使用工厂+注解方式动态配置策略(利用Spring加载)

其中第1、2点请参见org.landy.strategy 包下的demo事例即可,而第4点的方式其实就是结合第1、2、3点的优点进行整合的方式。

2.3.2 注解方式优点

使用注解方式可以极大的减少使用模版方法模式带来的扩展时需要继承模版类的弊端,工厂+注解的方式可以无需关心其他业务类的实现,而且减少了类膨胀的风险。

2.3.3 组合主键查询策略

本文以组合主键查询策略这一策略进行说明,策略注解如下:

/**
 * 组合主键查询策略(根据不同业务流程区分组合主键查询策略,并且每个业务流程都有自己的优先级策略)
 * @author landyl
 * @create 2:22 PM 09/29/2018
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface KeyIdentificationStrategy {

    /**
     * 主键策略优先级
     * @return
     */
    int priority() default 0;
    /**
     * 业务流程类型(如:订单信息,会员信息等业务流程)
     * @return
     */
    WorkflowEnum workflowId();
    /**
     * the spring bean name
     * @return
     */
    String beanName();

}

2.3.4 策略工厂

既然定义了组合主键查询策略注解,那必然需要一个注解处理器进行解析注解的操作,本文以工厂的方式进行。主要逻辑如下:

  1. 扫描指定包下的Java类,找出相应接口(即KeyIdentification)下的所有Class对象。private List<Class<? extends KeyIdentification>> getIdentifications() { Set<String> packageNames = this.getBasePackages(); List<Class<? extends KeyIdentification>> identifications = new ArrayList<>(); if(packageNames != null) { packageNames.forEach((packageName) -> identifications.addAll(getIdentifications(packageName))); } return identifications; }
  2. 解析注解KeyIdentificationStrategy,定义一个排序对象(KeyIdentificationComparator),指定优先级。private class KeyIdentificationComparator implements Comparator { @Override public int compare(Object objClass1, Object objClass2) { if(objClass1 != null && objClass2 != null) { Optional<KeyIdentificationStrategy> strategyOptional1 = getPrimaryKeyIdentificationStrategy((Class)objClass1); Optional<KeyIdentificationStrategy> strategyOptional2 = getPrimaryKeyIdentificationStrategy((Class)objClass2); KeyIdentificationStrategy ip1 = strategyOptional1.get(); KeyIdentificationStrategy ip2 = strategyOptional2.get(); Integer priority1 = ip1.priority(); Integer priority2 = ip2.priority(); WorkflowEnum workflow1 = ip1.workflowId(); WorkflowEnum workflow2 = ip2.workflowId(); //先按业务类型排序 int result = workflow1.getValue() - workflow2.getValue(); //再按优先级排序 if(result == 0) return priority1.compareTo(priority2); return result; } return 0; } }
  3. 根据注解,把相应业务类型的组合主键查询策略对象放入容器中(即DefaultKeyIdentificationChain)。KeyIdentificationStrategy strategy = strategyOptional.get(); String beanName = strategy.beanName(); //业务流程类型 WorkflowEnum workflowId = strategy.workflowId(); KeyIdentificationStrategy priority = getPrimaryKeyIdentificationStrategy(v).get(); LOGGER.info("To add identification:{},spring bean name is:{},the identify priority is:{},workflowId:{}",simpleName,beanName,priority.priority(),workflowId.name()); KeyIdentification instance = ApplicationUtil.getApplicationContext().getBean(beanName,v); defaultKeyIdentificationChain.addIdentification(instance,workflowId);
  4. 后续,在各自对应的业务查询组件对象中即可使用该工厂对象调用如下方法,即可进行相应的查询操作。
public IdentificationResultType identify(IdentifyCriterion identifyCriterion,WorkflowEnum workflowId) {
        defaultKeyIdentificationChain.doClearIdentificationIndex(workflowId);
        return defaultKeyIdentificationChain.doIdentify(identifyCriterion,workflowId);
    }

3. 动态代理+Spring AOP

AOP设计模式通常运用在日志,校验等业务场景,本文将简单介绍基于Spring的AOP代理模式的运用。

3.1 Spring AOP

3.1.1 Spring AOP原理

AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。本文以Spring AOP的实现进行分析和介绍。

Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

注意:以上片段引用自文章Spring AOP的实现原理,如有冒犯,请联系笔者删除之,谢谢!

Spring AOP判断是JDK代理还是CGLib代理的源码如下(来自org.springframework.aop.framework.DefaultAopProxyFactory):

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                                         "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }
}

由代码发现,如果配置proxyTargetClass = true了并且目标类非接口的情况,则会使用CGLib代理,否则使用JDK代理。

3.1.2 Spring AOP配置

Spring AOP的配置有两种方式,XML和注解方式。

3.1.2.1 XML配置

首先需要引入AOP相关的DTD配置,如下:

<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
		">

然后需要引入AOP自动代理配置:

<!-- 自动扫描(自动注入) -->
<context:component-scan base-package="org.landy" />
<!-- 指定proxy-target-class为true可强制使用cglib -->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

3.1.2.2 注解配置

Java配置类如下:

/**
 * 相当于Spring.xml配置文件的作用
 * @author landyl
 * @create 2:44 PM 09/30/2018
 */
@Configuration
//@EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
@EnableAspectJAutoProxy(proxyTargetClass = true)
//@EnableAspectJAutoProxy
@ComponentScan(basePackages = "org.landy")
public class ApplicationConfigure {

    @Bean
    public ApplicationUtil getApplicationUtil() {
        return new ApplicationUtil();
    }

}

3.1.2.3 依赖包

需要使用Spring AOP需要引入以下Jar包:

<properties>
    <spring.version>5.0.8.RELEASE</spring.version>
    <aspectj.version>1.8.7</aspectj.version>
</properties>
<!-- aspectjrt.jar包主要是提供运行时的一些注解,静态方法等等东西,通常我们要使用aspectJ的时候都要使用这个包。 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${aspectj.version}</version>
</dependency>
<!-- aspectjweaverjar包主要是提供了一个java agent用于在类加载期间织入切面(Load time weaving)。
    并且提供了对切面语法的相关处理等基础方法,供ajc使用或者供第三方开发使用。这个包一般我们不需要显式引用,除非需要使用LTW。
     -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.version}</version>
    <scope>compile</scope>
</dependency>

3.1.2.4 配置单元测试

以上两种配置方式,单元测试需要注意一个地方就是引入配置的方式不一样,区别如下:

  1. XML方式@ContextConfiguration(locations = { "classpath:spring.xml" }) //加载配置文件 @RunWith(SpringJUnit4ClassRunner.class) //使用junit4进行测试 public class SpringTestBase extends AbstractJUnit4SpringContextTests { }
  2. 注解方式
@ContextConfiguration(classes = ApplicationConfigure.class)
@RunWith(SpringJUnit4ClassRunner.class)  //使用junit4进行测试
public class SpringTestBase extends AbstractJUnit4SpringContextTests {
}

配置好了以后,以后所有的测试类都继承SpringTestBase类即可。

3.2 项目演示

3.2.1 逻辑梳理

本文将以校验某个业务逻辑为例说明Spring AOP代理模式的运用。

按照惯例,还是以客户信息更新校验为例,假设有个校验类如下:

/**
 * @author landyl
 * @create 2:22 PM 09/30/2018
 */
@Component
public class CustomerUpdateRule implements UpdateRule {

    //利用自定义注解,进行AOP切面编程,进行其他业务逻辑的校验操作
    @StatusCheck
    public CheckResult check(String updateStatus, String currentStatus) {
        System.out.println("CustomerUpdateRule:在此还有其他业务校验逻辑。。。。"+updateStatus + "____" + currentStatus);
        return new CheckResult();
    }

}

此时我们需要定义一个注解StatusCheck类,如下:

/**
 * @author landyl
 * @create 2:37 PM 09/23/2018
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface StatusCheck {

}

此注解仅为一个标记注解。最为主要的就是定义一个更新校验的切面类,定义好切入点。

@Component
@Aspect
public class StatusCheckAspect {
    private static final int VALID_UPDATE = Constants.UPDATE_STATUS_VALID_UPDATE;

    private static final Logger LOGGER = LoggerFactory.getLogger(StatusCheckAspect.class);


    //定义切入点:定义一个方法,用于声明切面表达式,一般地,该方法中不再需要添加其他的代码
    @Pointcut("execution(* org.landy.business.rules..*(..)) && @annotation(org.landy.business.rules.annotation.StatusCheck)")
    public void declareJoinPointExpression() {}

    /**
     * 前置通知
     * @param joinPoint
     */
    @Before("declareJoinPointExpression()")
    public void beforeCheck(JoinPoint joinPoint) {
        System.out.println("before statusCheck method start ...");
        System.out.println(joinPoint.getSignature());
        //获得自定义注解的参数
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("The method " + methodName + " begins with " + args);
        System.out.println("before statusCheck method end ...");
    }
}    

具体代码请参见github。

3.2.2 逻辑测试

3.2.2.1 JDK动态代理

JDK动态代理必须实现一个接口,本文实现UpdateRule为例,

public interface UpdateRule {
    CheckResult check(String updateStatus, String currentStatus);
}

并且AOP需要做如下配置:

XML方式:

<!-- 指定proxy-target-class为true可强制使用cglib -->
<aop:aspectj-autoproxy proxy-target-class="false"></aop:aspectj-autoproxy>

注解方式:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "org.landy")
public class ApplicationConfigure {

}

在测试类中,必须使用接口方式注入:

/**
 * @author landyl
 * @create 2:32 PM 09/30/2018
 */
public class CustomerUpdateRuleTest extends SpringTestBase {

    @Autowired
    private UpdateRule customerUpdateRule; //JDK代理方式必须以接口方式注入

    @Test
    public void customerCheckTest() {
        System.out.println("proxy class:" + customerUpdateRule.getClass());
        CheckResult checkResult = customerUpdateRule.check("2","currentStatus");
        AssertUtil.assertTrue(checkResult.getCheckResult() == 0,"与预期结果不一致");
    }

}

测试结果如下:

proxy class:class com.sun.proxy.$Proxy34
2018-10-05  14:18:17.515 [main] INFO  org.landy.business.rules.aop.StatusCheckAspect - Status check around method start ....
before statusCheck method start ...
CheckResult org.landy.business.rules.stategy.UpdateRule.check(String,String)
The method check begins with [2, currentStatus]
before statusCheck method end ...
CustomerUpdateRule:在此还有其他业务校验逻辑。。。。2____currentStatus
2018-10-05  14:18:17.526 [main] INFO  org.landy.business.rules.aop.StatusCheckAspect - execute the target method,the return result_msg:null
2018-10-05  14:18:17.526 [main] INFO  org.landy.business.rules.aop.StatusCheckAspect - Status check around method end ....

以上结果说明它生成的代理类为$Proxy34,说明是JDK代理。

3.2.2.2 CGLib动态代理

使用CGlib可以不用接口(经测试,用了接口好像也没问题)。在测试类中,必须使用实现类方式注入:

 @Autowired
 private CustomerUpdateRule customerUpdateRule;

并且AOP需要做如下配置:

XML方式:

<!-- 指定proxy-target-class为true可强制使用cglib -->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

注解方式:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "org.landy")
public class ApplicationConfigure {

}

不过发现我并未配置proxyTargetClass = true也可以正常运行,有点奇怪。(按理说,默认是为false)

运行结果生成的代理类为:

proxy class:class org.landy.business.rules.stategy.CustomerUpdateRule$EnhancerBySpringCGLIB$d1075aca

说明是CGLib代理。

经过进一步测试,发现如果我实现接口UpdateRule,但是注入方式使用类注入方式:

@Autowired
private CustomerUpdateRule customerUpdateRule;

并且把proxyTargetClass设置为false,则运行就报如下错误:

严重: Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@6a024a67] to prepare test instance [org.landy.business.rules.CustomerUpdateRuleTest@7fcf2fc1]
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.landy.business.rules.CustomerUpdateRuleTest': Unsatisfied dependency expressed through field 'customerUpdateRule'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'customerUpdateRule' is expected to be of type 'org.landy.business.rules.stategy.CustomerUpdateRule' but was actually of type 'com.sun.proxy.$Proxy34'
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:586)

以上说明了一个问题,使用接口实现的方式则会被默认为JDK代理方式,如果需要使用CGLib代理,需要把proxyTargetClass设置为true。

3.2.2.3 综合测试

为了再次验证Spring AOP如何选择JDK代理还是CGLib代理,在此进行一个综合测试。

测试前提:

  1. 实现UpdateRule接口
  2. 测试类使用接口方式注入@Autowired private UpdateRule customerUpdateRule; //JDK代理方式必须以接口方式注入

测试:

配置proxyTargetClass为true,运行结果如下:

customerCheckTest
proxy class:class org.landy.business.rules.stategy.CustomerUpdateRule$EnhancerBySpringCGLIB$f5a34953
2018-10-05  15:28:42.820 [main] INFO  org.landy.business.rules.aop.StatusCheckAspect - Status check around method start ....
2018-10-05  15:28:42.823 [main] INFO  org.landy.business.rules.aop.StatusCheckAspect - Status check dynamic AOP,paramValues:2
AOP实际校验逻辑。。。。2----currentStatus
before statusCheck method start ...
target class:org.landy.business.rules.stategy.CustomerUpdateRule@7164ca4c

说明为CGLIb代理。

配置proxyTargetClass为false,运行结果如下:

proxy class:class com.sun.proxy.$Proxy34
2018-10-05  15:20:59.894 [main] INFO  org.landy.business.rules.aop.StatusCheckAspect - Status check around method start ....
before statusCheck method start ...
target class:org.landy.business.rules.stategy.CustomerUpdateRule@ae3540e

说明为JDK代理。

以上测试说明,指定proxy-target-class为true可强制使用cglib。

3.2.3 常见问题

如果使用JDK动态代理,未使用接口方式注入(或者使用接口实现,并未配置proxyTargetClass为true),则会出现以下异常信息:

严重: Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@6a024a67] to prepare test instance [org.landy.business.rules.CustomerUpdateRuleTest@7fcf2fc1]
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.landy.business.rules.CustomerUpdateRuleTest': Unsatisfied dependency expressed through field 'customerUpdateRule'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'customerUpdateRule' is expected to be of type 'org.landy.business.rules.stategy.CustomerUpdateRule' but was actually of type 'com.sun.proxy.$Proxy34'

与生成的代理类型不一致,有兴趣的同学可以Debug DefaultAopProxyFactory类中的createAopProxy方法即可自动两种动态代理的区别。

上一篇

已是最后文章

下一篇

已是最新文章

发表回复