Spring AOP 使用与分析

[TOC]

AOP简介

理解

AOP(Aspect-Oriented Programming), 即 面向切面编程,其基本思想是在极少影响原程序的代码的前提下,在程序中的某些地方,使用某些方式,不可见的(即不在原程序中添加其他代码)为原程序切入一些额外的功能。

优点

  • 减少代码间的耦合性,使功能具有拔插性,保证自己代码的清洁性。
  • 能够让你只关注自己的代码,不需要关注切面是如何实现的。

术语

通知(advice)

其定义了切点什么时候去增强,是在方法调用前,还是调用之后,还是前后都是,还是抛出异常时。

  • Before 某方法调用之前发出通知。
  • After 某方法完成之后发出通知,不考虑方法运行的结果。
  • After-returning 将通知放置在被通知的方法成功执行之后。
  • After-throwing 将通知放置在被通知的方法抛出异常之后。
  • Around 通知包裹在被通知的方法的周围,在方法调用之前和之后发出通知
连接点(join point)

可以被作为切点的地方,都可以被认为是链接点。

切点(point cut)

按照规则被选中的链接点,可以被称作为切点。

Aspect(切面)

aspectpointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.

目标对象(Target)

织入 advice 后的目标对象. 目标对象也被称为 advised object.

引入(introductions):
  • 引入允许你添加一个新的方法给已经存在的类。

Spring对AOP的支持

  • Spring建议在Java中书写AOP
  • Spring是在运行阶段才将切面编织进bean中,是使用代理类。
  • Spring只支持方法级别的连接点。

AOP应用

XML形式的AOP

proxy-target-class="true"指定使用GCLIB代理,如果proxy-target-class="false"或者没设置,则默认使用动态代理,但是如果代理类没有实现接口,则依然会使用GCLIB代理。

aop:pointcut指定了切点。

aop:advisor指定了通知时机,同样的还有aop:before aop:after

需要注意的是spiritCommonInterceptor实现了MethodInterceptor接口

   <bean id="spiritCommonInterceptor" class="com.mogujie.stable.spirit.point.methond.CommonInterceptor"/>
    <aop:config proxy-target-class="true">
        <aop:pointcut id="modulePoint" expression="@target(com.mogujie.stable.spirit.point.annotation.ClassSpirit) and @annotation(com.mogujie.stable.spirit.point.annotation.MethodSpirit)"/>
        <aop:advisor advice-ref="spiritCommonInterceptor" pointcut-ref="modulePoint"/>
    </aop:config>
public class CommonInterceptor implements MethodInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(CommonInterceptor.class);

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method executed = invocation.getMethod();
        Class<?> clazz = invocation.getThis().getClass();
        ClassSpirit classSpirit = clazz.getAnnotation(ClassSpirit.class);
        MethodSpirit methodSpirit = executed.getAnnotation(MethodSpirit.class);
        // 不做限流降级处理
        if (executed.getName().equals("toString") || executed.getName().equals("hashCode") || executed.getName().equals("equals") || (null != classSpirit && !classSpirit.trace()) || (null == classSpirit && null != methodSpirit && !methodSpirit.trace()) || ((null != classSpirit && classSpirit.trace()) && (null == methodSpirit || !methodSpirit.trace()))) {

            return invocation.proceed();
        }
        Entry entry = null;
        try {
            String methodName = MethodUtil.getMethodName(executed);

            // 初始化Context
            ContextUtil.enter(methodName);
            // 初始化Entry
            entry = EntryUtil.entry(executed);
            // 执行方法
            Object result = invocation.proceed();
            return result;
        } catch (Throwable e) {
            throw ExceptionUtil.dealProxyException(e);
        } finally {
            if (entry != null) {
                entry.exit();
            }
            ContextUtil.exit();
        }
    }

}

Annotation形式的AOP

Spring除了支持Schema方式配置AOP,还支持注解方式:使用@Aspect来配置。但Spring默认不支持@Aspect风格的切面声明,通过如下配置开启@Aspect支持:

<aop:aspectj-autoproxy/>  
package com.sxit;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectStyle {
    
    @Pointcut("execution(* com.sxit..*.*(..))")
    public void init(){
        
    }

    @Before(value="init()")
    public void before(){
        System.out.println("方法执行前执行.....");
    }
    
    @AfterReturning(value="init()")
    public void afterReturning(){
        System.out.println("方法执行完执行.....");
    }
    
    @AfterThrowing(value="init()")
    public void throwss(){
        System.out.println("方法异常时执行.....");
    }
    
    @After(value="init()")
    public void after(){
        System.out.println("方法最后执行.....");
    }
    
    @Around(value="init()")
    public Object around(ProceedingJoinPoint pjp){
        System.out.println("方法环绕start.....");
        Object o = null;
        try {
            o = pjp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("方法环绕end.....");
        return o;
    }
}

一个Annotation与AOP结合的例子

要实现一个aop的功能,关键在于三个地方。

  • 通知(Advice) 定义了何时切。比如:before、around等。
  • 切点(PointCut) 定义了何处切。比如:execution(* com.mogujie.houston.openapi.api.impl..*(..))
  • 连接点(JoinPoint) 连接点是在应用执行过程中能够插入切面的一个点。能够利用它拿到应用的方法和参数等。
    aspect
@Aspect
@Component
public class ValidatorAspect implements ApplicationContextAware {

    private static Logger logger = LoggerFactory.getLogger(ValidatorAspect.class);

    protected static ApplicationContext context;

    @Around("execution(* com.mogujie.houston.openapi.api.impl..*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            if (method.getDeclaringClass().isInterface()) {
                try {
                    method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
                            method.getParameterTypes());
                } catch (final SecurityException exception) {
                }
            }
            // check passport
            Object[] args = joinPoint.getArgs();
            Validator validatorClass = method.getAnnotation(Validator.class);
            if (null != validatorClass) {
                ValidationHandler validationHandler = validatorClass.handler().newInstance();
                HoustonOpenApiResult result = validationHandler.validate(args);
                if (!result.isSuccess()) {
                    return result;
                }
            }
            TokenValidator tokenValidatorClass = method.getAnnotation(TokenValidator.class);
            if (tokenValidatorClass != null) {
                TokenValidationHandler tokenValidationHandler = tokenValidatorClass.handler().newInstance();
                HoustonOpenApiResult result = tokenValidationHandler.check(args, context);
                if (!result.isSuccess()) {
                    return result;
                }
            }

        } catch (Exception e) {
            logger.error("ValidatorAspect验证出现异常", e);
            return HoustonOpenApiResult.error(OpenApiResultCode.INNER_ERROR, "系统异常 请@Houston答疑, error:" + e.getMessage());
        }
        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            logger.error("Service服务出现异常", e);
            return HoustonOpenApiResult.error(OpenApiResultCode.INNER_ERROR, "Service出现异常 请@Houston答疑, error:" + e.getMessage());
        }

    }

    public static ApplicationContext getContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ValidatorAspect.context = applicationContext;
    }
}

@TokenValidator

@Documented
@Target({ElementType.METHOD})//只能在方法上使用
@Retention(RetentionPolicy.RUNTIME)//运行时使用
public @interface TokenValidator {

    Class<? extends TokenValidationHandler> handler() default TokenValidationHandler.class;//定义了一个接口类

}

TokenValidationHandler

public interface TokenValidationHandler {
    HoustonOpenApiResult check(Object[] args, ApplicationContext applicationContext);

    HoustonOpenApiResult getGroupResult(Object[] args, DefaultGroupBiz defaultGroupBiz);
}

一个Handler的实现

public abstract class BaseTVHandler implements TokenValidationHandler {


    @Override
    public HoustonOpenApiResult check(Object[] args, ApplicationContext context) {
        if (args.length >= 2) {
            Token token = (Token) args[0];
            DefaultGroupBiz defaultGroupBiz = context.getBean(DefaultGroupBiz.class);
            HoustonOpenApiResult<Group> groupResult = getGroupResult(args, defaultGroupBiz);
            if (!groupResult.isSuccess()) {
                return groupResult;
            }
            if (TokenUtil.check(token, groupResult.getData().getKeyName())) {
                return HoustonOpenApiResult.success(true);
            }
            return new HoustonOpenApiResult(OpenApiResultCode.TOKEN_ILLEGAL);
        } else {
            return new HoustonOpenApiResult(OpenApiResultCode.TOKEN_PARAM_ERROR);
        }
    }


}
public class ConfigValueTokenValidator {

    public static class DetailHandler extends BaseTVHandler {

        @Override
        public HoustonOpenApiResult getGroupResult(Object[] args, DefaultGroupBiz defaultGroupBiz) {
            ConfigValueDetail configValueDetail = (ConfigValueDetail) args[1];
            return defaultGroupBiz.queryByConfigId(configValueDetail.getConfigId());
        }

    }
}

AOP原理

Spring AOP使用了两种代理机制:一种是基于JDK的动态代理;另一种是基于CGLib的动态代理。之所以需要两种代理机制,很大程度上是因为JDK本身只提供接口的代理,而不支持类的代理。

JDK动态代理

步骤

  1. 通过实现InvocationHandler接口创建自己的调用处理器
  2. 通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理类
  3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型
  4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入

public class DynamicTest implements InvocationHandler {


    private Test target;

    private DynamicTest(Test target) {
        this.target = target;
    }

    public static Test newProxyInstance(Test test) {
        return (Test) Proxy.newProxyInstance(test.getClass().getClassLoader(), test.getClass().getInterfaces(), new DynamicTest(test));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }

}

基于CGLIB的动态代理

CGLIB直接生成代理目标类的子类,不能对目标类中的final方法进行代理

  1. 查找A上的所有非final 的public类型的方法定义;

  2. 将这些方法的定义转换成字节码;

  3. 将组成的字节码转换成相应的代理的class对象;

  4. 实现 MethodInterceptor接口,用来处理 对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)

public class CglibTest implements MethodInterceptor {

    private CglibTest() {
    }

    public static <T extends Test> Test newProxyInstance(Class<T> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);
        enhancer.setCallback(new CglibTest());
        return (Test) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return methodProxy.invokeSuper(o, objects);
    }

}

ASM(介绍)

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。

Javassist(介绍)

Javassist是一款字节码编辑工具,可以直接编辑和生成Java生成的字节码,以达到对.class文件进行动态修改的效果。熟练使用这套工具,可以让Java编程更接近与动态语言编程。

JDK动态代理与CGLIB性能比较

  1. 被代理接口
public interface Test {

    public int test(int i);
}

  1. 实现类
public class TestImpl implements Test {
    @Override
    public int test(int i) {
        return i + 1;
    }

    public void print() {
        System.out.println("111111");
    }

}
  1. JDK代理类
public class DynamicTest implements InvocationHandler {


    private Test target;

    private DynamicTest(Test target) {
        this.target = target;
    }

    public static Test newProxyInstance(Test test) {
        return (Test) Proxy.newProxyInstance(test.getClass().getClassLoader(), test.getClass().getInterfaces(), new DynamicTest(test));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }

}
  1. CGLIB代理类
public class CglibTest implements MethodInterceptor {

    private CglibTest() {
    }

    public static <T extends Test> Test newProxyInstance(Class<T> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);
        enhancer.setCallback(new CglibTest());
        return (Test) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return methodProxy.invokeSuper(o, objects);
    }
}
  1. 测试类
public class ProxyPerfTester {

    public static void main(String[] args) {
        //创建测试对象;
        Test nativeTest = new TestImpl();
        Test dynamicProxy = DynamicTest.newProxyInstance(nativeTest);
        Test cglibProxy = CglibTest.newProxyInstance(TestImpl.class);

        //预热一下;
        int preRunCount = 100000;
        runWithoutMonitor(nativeTest, preRunCount);
        runWithoutMonitor(cglibProxy, preRunCount);
        runWithoutMonitor(dynamicProxy, preRunCount);

        //执行测试;
        Map<String, Test> tests = new LinkedHashMap<String, Test>();
        tests.put("Native   ", nativeTest);
        tests.put("Dynamic  ", dynamicProxy);
        tests.put("Cglib    ", cglibProxy);
        int repeatCount = 3;
        int runCount = 1000000;
        runTest(repeatCount, runCount, tests);
        runCount = 50000000;
        runTest(repeatCount, runCount, tests);
    }

    private static void runTest(int repeatCount, int runCount, Map<String, Test> tests){
        System.out.println(String.format("\n==================== run test : [repeatCount=%s] [runCount=%s] [java.version=%s] ====================", repeatCount, runCount, System.getProperty("java.version")));
        for (int i = 0; i < repeatCount; i++) {
            System.out.println(String.format("\n--------- test : [%s] ---------", (i+1)));
            for (String key : tests.keySet()) {
                runWithMonitor(tests.get(key), runCount, key);
            }
        }
    }

    private static void runWithoutMonitor(Test test, int runCount) {
        for (int i = 0; i < runCount; i++) {
            test.test(i);
        }
    }

    private static void runWithMonitor(Test test, int runCount, String tag) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < runCount; i++) {
            test.test(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("["+tag + "] Elapsed Time:" + (end-start) + "ms");
    }
}
  1. 结果
Create Native Proxy:1ms
Create Dynamic Proxy17ms
Create Cglib Proxy521ms

==================== run test : [repeatCount=3] [runCount=1000000] [java.version=1.7.0_79] ====================

--------- test : [1] ---------
[Native   ] Elapsed Time:7ms
[Dynamic  ] Elapsed Time:289ms
[Cglib    ] Elapsed Time:93ms

--------- test : [2] ---------
[Native   ] Elapsed Time:7ms
[Dynamic  ] Elapsed Time:12ms
[Cglib    ] Elapsed Time:51ms

--------- test : [3] ---------
[Native   ] Elapsed Time:6ms
[Dynamic  ] Elapsed Time:14ms
[Cglib    ] Elapsed Time:45ms

==================== run test : [repeatCount=3] [runCount=50000000] [java.version=1.7.0_79] ====================

--------- test : [1] ---------
[Native   ] Elapsed Time:468ms
[Dynamic  ] Elapsed Time:1855ms
[Cglib    ] Elapsed Time:1577ms

--------- test : [2] ---------
[Native   ] Elapsed Time:165ms
[Dynamic  ] Elapsed Time:418ms
[Cglib    ] Elapsed Time:807ms

--------- test : [3] ---------
[Native   ] Elapsed Time:161ms
[Dynamic  ] Elapsed Time:484ms
[Cglib    ] Elapsed Time:889ms

可见在JDK1.7下:

  • 运行速度,Native是最快的,JDK动态代理稍次之,CGLIB最慢。
  • 创建速度,Native是最快的,JDK动态代理稍次之,CGLIB最慢。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容