作为鹅厂新晋实习生,已经来鹅厂一个月啦,虽然一直在团队中打杂写单测,但也学到了很多东西,比如最近在看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类的方法,做到了方法的拦截。