Spring AOP在单测中的应用

Posted by     "Eric" on Friday, May 15, 2020

作为鹅厂新晋实习生,已经来鹅厂一个月啦,虽然一直在团队中打杂写单测,但也学到了很多东西,比如最近在看Spring AOP相关的知识,也刚好看到了工程中前人写的一些单测代码,也算是加强了对AOP的理解和认识,这里写一点总结和感悟,还是那句话,纸上得来终觉浅绝知此事要躬行。关于AOP的理论知识,可以看一下这篇文章,这篇文章中对aop的四个概念——aspect, join point, point cut, advice有着非常精彩的比喻。

在做单测的时候,有些方法可能会连接网络,或者调用数据库这类“重”资源,影响单测效率是一方面,更多的时候我们没有这些资源,所以我们常常选用mock对象的方法,跳过或者规避这类调用。

我一般使用PowerMockito或者Mockito这类第三方框架来实现,组里的前辈们则是使用了Spring AOP的机制来实现的,还是很有趣的,代码贴在下方。

ApiDataResourceService这个类中的queryById这个方法通过传入一个dataResourceId值,通过http从网上得到该数据资源的json资源,然后将其转换为DataResource这个类。

@Service
public class ApiDataResourceService extends MetadataRequestService {

    /**
     * 查询完整数据源
     *
     * @param dataResourceId
     * @return
     */
    public DataResource queryById(Integer dataResourceId) {
		// 通过http获得json文件,返回DataResource
    }
}

因为需要从网上拖取json资源,单测时总是存在网络波动等情况影响单测的稳定性,于是就写了一个Mock类,继承自原有得了类,并重写了方法,使其直接从本地读取json文件(json文件已经下载在本地)

@MockObject
public class MockApiDataResourceService extends ApiDataResourceService {
    
    @Override
    public DataResource queryById(Integer dataResourceId) {
        // 从本地拖拉DataResource的json文件,返回DataResource
    }
}

@MockObject注解是自定义的一个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MockObject {

}

Aspect(切面)类如下:

/**
 * 拦截Dao,使用mock.dao下的类替代真正的dao返回数据.
 *
 */
@Component
@Aspect
@Slf4j
public class TestServiceAspectAdvice implements Ordered {
    private Map<Class<?>, Class<?>> services = new HashMap<>();

    public TestServiceAspectAdvice() {
        super();
        // 在包中(Pkg)收集所有被@MockObject标注的类
        // 并放在services Map中,key是父类型 value是mock的对象
        Set<Class<?>> servicesSet = PkgUtil.
            getClzFromPkg("com.tencent.beacon.analysis.controller.mock.service");
        servicesSet.stream().filter(s -> null != s.getAnnotation(MockObject.class)).
            forEach(s -> {
            log.info("======= found: " + s.getName());
            log.info("======= found: " + s.getSuperclass().getName());
            services.put((Class<?>) s.getSuperclass(), s);
        });
    }

    public static void main(String[] args) {
        new TestServiceAspectAdvice();
    }

    // 使用around的方式织入
    @SuppressWarnings({"unchecked", "rawtypes"})
    @Around(value = "execution(* com.tencent.beacon.analysis.service..*(..))")
    // 从jp中可以得到point cut的各种信息
    public Object doServiceAround(ProceedingJoinPoint jp) throws Throwable {
        try {
            Method newMethod;
            // 遍历所有的mock类
            for (Class<?> i : services.keySet()) {
                // 得到point cut的类对象
                Class jpClass = jp.getSignature().getDeclaringType();
                // 判断joint point类是否在services里(即是否有mock类继承自该类)
                if (jpClass.isAssignableFrom(i)) { 
                    log.info("======== mock service: " + i);
                    Signature sig = jp.getSignature();
                    MethodSignature msig = null;
                    if (sig instanceof MethodSignature) {
                        msig = (MethodSignature) sig;
                        Object target = jp.getTarget();  // 得到拦截对象的方法
                        Method currentMethod = target.getClass().
                            getMethod(msig.getName(), msig.getParameterTypes()); // 通过反射得到拦截方法的方法对象
                        Class<?> clazz = services.get(i);  // 拿到mock类
                        Object object = clazz.newInstance(); // 创建mock类的对象
                        newMethod = clazz.getMethod(currentMethod.getName(), currentMethod.getParameterTypes());    // 调用mock类中的方法
                        return newMethod.invoke(object, jp.getArgs());
                    }
                }
            }
            return jp.proceed(jp.getArgs());
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }
}

总结起来,整个流程是:继承旧类,生成mock类,并重写方法——>在Aspect类中扫描包,建立旧类和mock类的映射——>定义Advice方法,被调用时通过jointPoint参数,首先判断该类是否有mock类,如果有的话得到要调用的方法(方法名,参数等),然后调用其mock类的方法,做到了方法的拦截。