最近在重构项目,有一个场景:根据方法的返回值判断是否成功,成功则从入参里获取需要的参数,构造消息(Msg)并发送通知其他模块.
因为是比较通用的逻辑(判断返回值,获取参数,发送消息),因此做成Spring注解的形式,注解中会用AOP拦截方法获取方法的返回值和参数.
其中获取参数使用的是Spring SpEL表达式.
1.使用背景
碰到的一个问题是AOP拦截无法获取private,protected和inner方法的参数.使用场景如下:
1.1 注解定义
首先定义注解用于获取
1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
/**
* 参数定义,标识了参数的获取方式,或常量值
*/
String[] params();
/**
* 操作类型,标识了当前这注解该被哪个处理类来处理
*/
String[] operate();
}
1.2 定义切面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
public class testAspect {
/**
* 根据注解获取切面
*/
@Pointcut("@annotation(com.aaa.bbb.ccc.ParamGrabber)")
public void aspectPointCut() {
}
/**
* AOP, 定义方法返回之后的操作
*/
@AfterReturning(value = "aspectPointCut()", returning = "returnValue")
public void afterReturning(JoinPoint point, Object returnValue){
//1.获取拦截的类和方法
//2.获取方法上我们定义的特定注解
//3.根据注解的参数中的spel表达式,从方法入参中获取我们需要的参数
//4.一些列的Processor处理逻辑
}
}
1.3 处理类
定义获取的参数之后的处理类的逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Processor {
/**
* 本processor可以处理的操作类型
*/
List<String> supportOperates();
/**
*
* 此processor的处理逻辑
*/
void process(XxxContext context);
/**
* 返回值验证,是否需要执行process操作
*/
boolean isReturnValid(XxxContext context, Object returnValue);
}
之后利用Spring就可以获取上下问中所有的实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ProcessorSupport implements ApplicationContextAware, InitializingBean {
// Spring上下文
private ApplicationContext applicationContext;
private final Multimap<String, Processor> processorMap = HashMultimap.create();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
// 从Spring上下文获取Processor的所有具体实现类
Map<String, Processor> beansOfType = applicationContext.getBeansOfType(Processor.class);
for(Processor processor : beansOfType.values()) {
for(String op : processor.supportOperates()) {
processorMap.put(op, processor);
}
}
}
//根据注解中定义的operate来获取具体的处理类Processor
@Override
public Collection<Processor> getProcessor(String operate) {
return processorMap.get(operate);
}
}
1.4 使用
使用的时候,首先得实现一个具体的Processor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyProcessor implements Processor {
public static final String SPECIFIC_PROCESSOR_KEY = "SPECIFIC_PROCESSOR_KEY";
@Override
public List<String> supportOperates() {
return ImmutableList.of(
SPECIFIC_PROCESSOR_KEY
);
}
@Override
public void process(XxxContext context) {
//获取注解解析得到的参数
Map<String, Object> parsedParam = context.getParsedParam();
//构建发送的消息内容
Msg msg = buildNoticeMsg(parsedParam);
//发送消息
noticeService.doNotice(msg);
}
@Override
public boolean isReturnValid(XxxContext context, Object returnValue) {
//判断返回值是否有效...
}
}
具体使用的地方:
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据SPECIFIC_PROCESSOR_KEY找到MyProcessor
* params中使用SpEL获取参数中的某个属性
*/
@Param(operate = MyProcessor.SPECIFIC_PROCESSOR_KEY, params = {
CrateConstants.KEYS_KEY + "={#p0.![propertyName]}"})
public int batchInsert(List<Tag> tagList) {
if (CollectionUtils.isEmpty(tagList)) {
return 0;
}
return tagDao.insertBatchly(tagList);
}
2.问题
2.1 private和protected方法无效
private和protected方法(通常为类的inner方法)无法使用我们定义的注解来达到目的,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//供外部调用的方法
public class XxxService {
public int outerMethod(List<Tag> tagList) {
return innerMethod(tagList);
}
}
//被当前类的其他方法调用的内部方法
@Param(operate = MyProcessor.SPECIFIC_PROCESSOR_KEY, params = {
CrateConstants.KEYS_KEY + "={#p0.![propertyName]}"})
private int innerMethod(List<Tag> tagList) {
if (CollectionUtils.isEmpty(tagList)) {
return 0;
}
return tagDao.insertBatchly(tagList);
}
2.2 inner public方法无效
定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//供外部调用的方法
public class XxxService {
public int outerMethod(List<Tag> tagList) {
return innerMethod(tagList);
}
}
//被当前类的其他方法调用的内部方法
@Param(operate = MyProcessor.SPECIFIC_PROCESSOR_KEY, params = {
CrateConstants.KEYS_KEY + "={#p0.![propertyName]}"})
public int innerMethod(List<Tag> tagList) {
if (CollectionUtils.isEmpty(tagList)) {
return 0;
}
return tagDao.insertBatchly(tagList);
}
调用的时候调用
1
xxxServiceIns.outerMethod(...);
3.原因
3.1 private和protected方法无效
Spring的AOP框架基于代理实现的(proxy-based).使用原生的Java的代理是无法代理protected和private类型的方法(详见后续结束).
而CGLIB的代理虽然在技术上可以代理protected和private类型的方法,但是用于AOP的时候不推荐代理protected和private类型的方法.
结论就是,任何定义的pointcut只在public方法上有效.
参考:
http://stackoverflow.com/questions/15093894/aspectj-pointcut-for-annotated-private-methods
spring官方文档关于AOP的解释中也有相关的说明:
http://docs.spring.io/spring/docs/3.1.x/spring-framework-reference/html/aop.html#aop-introduction-spring-defn
3.2 inner public方法无效
原因其实就是一句话:Spring AOP是基于代理机制的.
考虑如下场景,当你拿到一个无代理的、无任何特殊之处的对象引用时:
1
2
3
4
5
6
7
8
9
10
public class SimplePojo implements SimpleInterface {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
当你调用一个对象引用的方法时,此对象引用上的方法直接被调用,如下所示:
1
2
3
4
5
public static void main(String[] args) {
SimplePojo ins = new SimplePojo();
// this is a direct method call on the 'ins' reference
ins.foo();
}
当客户代码所持有的引用是一个代理的时候则略有不同了,当调用foo()方法时候,首先会调用原始foo()方法上的@Before的代码逻辑,然后调用原始的foo()方法,原始foo()方法内的bar()调用的是原始对象的this.bar(),即一旦调用最终抵达了目标对象 (此处为SimplePojo类的引用),任何对自身的调用例如this.bar()将对this引用进行调用而非代理。
这一点意义重大,它意味着自我调用将不会导致和方法调用关联的AOP通知得到执行的机会。
1
2
SimplePojo proxy = proxyFactory.getProxy(Pojo.class);
proxy.foo();
参考:
http://m.oschina.net/blog/161387
4.Spring AOP代理
Spring AOP使用JDK动态代理或者CGLIB来为目标对象创建代理。
如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。 若该目标对象没有实现任何接口,则创建一个CGLIB代理。
如果你希望强制使用CGLIB代理,(例如:希望代理目标对象的所有方法,而不只是实现自接口的方法) 那也可以。但是需要考虑以下问题:
1.无法通知(advise)final方法,因为他们不能被覆写。
2.代理对象的构造器会被调用两次。因为在CGLIB代理模式下每一个代理对象都会 产生一个子类。每一个代理实例会生成两个对象:实际代理对象和它的一个实现了通知的子类实例 而是用JDK代理时不会出现这样的行为。通常情况下,调用代理类型的构造器两次并不是问题, 因为除了会发生指派外没有任何真正的逻辑被实现。
3.CGLib的效率没有使用JDK代理机制高,速度平均要慢8倍左右。
强制使用CGLIB代理需要将<aop:config>的proxy-target-class属性设为true:
1
2
3
<aop:config proxy-target-class="true">
...
</aop:config>
当使用@AspectJ自动代理时要强制使用CGLIB,请将<aop:aspectj-autoproxy>的proxy-target-class属性设置为true:
1
<aop:aspectj-autoproxy proxy-target-class="true"/>