Spring AOP理解与项目实战

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。下图就是AOP的核心概念和学习路线图,掌握此图是关键:

AOP架构图

一、方案举例

想象一下下,写了一个功能代码(比如SayHello()),想要在函数前后都做点什么,最简单的就是去写一段硬编码:

1.写死代码

这是功能的接口

public interface Greeting {
    void sayHello(String name);
}

在实现类里边去增加前置方法和后置方法

public class GreetingImpl implements Greeting { 
    @Override
    public void sayHello(String name) {
        before();
        System.out.println("Hello! " + name);
        after();
    }
    private void before() {
        System.out.println("Before");
    }
    private void after() {
        System.out.println("After");
    }
}

比如我们要统计每个方法的执行时间,以对性能作出评估,那是不是要在每个方法的一头一尾都做点手脚呢?这样写死的方法会累死码农们,于是来一个加强版。

2.静态代理

单独为 GreetingImpl 这个类写一个代理类,接口还是未变,实现类抽取出来放一边,这样就进行了解耦,后置和前置功能的实现放到这个静态代理类中去绑定结合:

//Greeting接口的实现类
public class GreetingImpl implements Greeting { 
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! " + name);
    }
}

//绑定前置和后置方法的静态代理类
public class GreetingProxy implements Greeting {
    private GreetingImpl greetingImpl;
    public GreetingProxy(GreetingImpl greetingImpl) {
        this.greetingImpl = greetingImpl;
    }
    @Override
    public void sayHello(String name) {
        before();
        greetingImpl.sayHello(name);
        after();
    }
    private void before() {
        System.out.println("Before");
    }
    private void after() {
        System.out.println("After");
    }
}

用这个 GreetingProxy 去代理 GreetingImpl,下面看看客户端如何来调用:

public class Client {
    public static void main(String[] args) {
        Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
        greetingProxy.sayHello("Jack");
    }
}

这样写没错,但是有个问题,每增强一个功能接口的实现类都要去实现一遍这个实现类的Proxy代理方法,最后的结果就会导致XxxProxy 这样的类会越来越多,如何才能将这些代理类尽可能减少呢?最好只有一个代理类。这时我们就需要使用 JDK 提供的动态代理了。

3.JDK代理

所有的代理类都合并到动态代理类中了,该代理类设置为泛型,可以接收各种实现类,经过代理类生成后可以绑定结合前置、后置方法,进行功能增强。

public class JDKDynamicProxy implements InvocationHandler {
 
    private Object target;
 
    public JDKDynamicProxy(Object target) {
        this.target = target;
    }
 
    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }
 
    private void before() {
        System.out.println("Before");
    }
 
    private void after() {
        System.out.println("After");
    }
}

客户端的调用

public class Client {
    public static void main(String[] args) {
        Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy();
        greeting.sayHello("Jack");
    }
}

所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK 给我们提供的动态代理只能代理接口,而不能代理没有接口的类。

4.cglib动态代理

我们使用开源的 CGLib 类库可以代理没有接口的类,这样就弥补了 JDK 的不足。

public class CGLibDynamicProxy implements MethodInterceptor {
 
    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
 
    private CGLibDynamicProxy() {
    }
 
    public static CGLibDynamicProxy getInstance() {
        return instance;
    }
 
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> cls) {
        return (T) Enhancer.create(cls, this);
    }
 
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object result = proxy.invokeSuper(target, args);
        after();
        return result;
    }
 
    private void before() {
        System.out.println("Before");
    }
 
    private void after() {
        System.out.println("After");
    }
}

客户端调用也更加轻松了

public class Client {
 
    public static void main(String[] args) {
        Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
        greeting.sayHello("Jack");
    }
}

二、AOP的概念

切面(Aspect):其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。

增强(Advice):是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。

连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但spring只支持方法级的连接点。

切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。

代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。

织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。还有引入(Introduction),用以区分类和方法的拦截。

1.项目aop的实现

项目中aop的实现基于两种模式或者综合,Spring AspectJ+(execution拦截表达式、基于@Annotation的注解拦截)

(1).Spring + AspectJ(基于注解:通过 AspectJ execution 表达式拦截方法)

通过表达式去匹配各个符合条件的切入点,如下就实现了aop.demo.GreetingImpl类下的任意方法拦截匹配,那么匹配后的切入点都会织入代理中。

@Aspect
@Component
public class GreetingAspect {
 
    @Around("execution(* aop.demo.GreetingImpl.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }
 
    private void before() {
        System.out.println("Before");
    }
 
    private void after() {
        System.out.println("After");
    }
}

类上面标注的 @Aspect 注解,这表明该类是一个 Aspect(其实就是 Advisor)。该类无需实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),只需在方法上标注 @Around 注解,在注解中使用了 AspectJ 切点表达式。方法的参数中包括一个 ProceedingJoinPoint 对象,它在 AOP 中称为 Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如:方法名、参数等。

虽然有了execution拦截表达式,但是有时候我们只想满足精确的某些方法,靠表达式难免会有缺漏或者是带入了一些不想带入的切入点进来,这可是很考验写拦截表达式的人,我们可以采用点对点的更精确的基于注解的方法。

(2).Spring + AspectJ(基于注解:通过 AspectJ @annotation 表达式拦截方法)

为了拦截指定的注解的方法,我们首先需要来自定义一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
}

以上定义了一个 @Tag 注解,此注解可标注在方法上,在运行时生效。

只需将前面的 Aspect 类的切点表达式稍作改动:

@Aspect
@Component
public class GreetingAspect {
 
    @Around("@annotation(aop.demo.Tag)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        ...
    }
    ...
}

这次使用了 @annotation() 表达式,只需在括号内定义需要拦截的注解名称即可。

直接将 @Tag 注解定义在您想要拦截的方法上,就这么简单:

@Component
public class GreetingImpl implements Greeting {
 
    @Tag
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! " + name);
    }
}

2.项目AOP实现

项目中可能就会同时采用Spring AspectJ的拦截表达式和注解拦截方法,假设要做一个操作日志的功能,需要对某些关键操作进行记录,将要记录操作日志的方法保存为枚举类。比如常见的增删改操作都要记录其操作:

public enum Type {
    DEFAULT("",""),
    ADD("1","新增"),
    UPDATE("2","修改"),
    DELETE("3","删除")
    ;
    private String id;
    private String operationType;
     Type(String id,String operationType){
        this.id=id;
        this.operationType=operationType;
    }

    public String getId() {return id;}

    public void setId(String id) {this.id = id;}

    public String getOperationType() {return operationType;}

    public void setOperationType(String operationType) {
         this.operationType = operationType;}
}

写一个注解类,该注解联合枚举就可以作为一个标记,每一个需要记录加一个对应注解,相应的操作就会留下操作类型信息,方便切面拦截下来调用处理。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface LogAnno {
    //操作类型
    public Type operationType() default Type.DEFAULT;
    //操作备注
    public String remark() default "";
}

切面类的编写,在这里我们采用了Spring AspectJ +两种拦截表达式,拦截特定类(execution(* main.com.*.*(..))下的加了特定注解(@annotation(main.com.LogAnno))的操作,可以进行后置加强方法操作和后置返回方法操作,一个是切下了切入点的输入信息,一个是切下了切入点返回值的信息,具体按照实际情景来各取所需吧。在拦下了输入参数信息的同时,我们可以采用反射来获取输入参数的属性和值,操作日志就相当于留下了现场的快照,我们只需要在写一个操作日志的写入数据库方法就可以保存下这些信息了。

/**
 * 切面类,含有多个通知
 */
@Aspect
@Component
public class MyAspect {
    //声明公共切入点
    @Pointcut("execution(* main.com.UserServiceImpl.*(..))")
    private void PointCut1(){}

    //声明指定包内的注解切入点
    @Pointcut("@annotation(main.com.LogAnno) && execution(* main.com.*.*(..)) ")
    private void PointCutofAnno(){}

    //拦截返回值
    @AfterReturning(value="PointCut1()" ,returning="ret")
    public void myAfterReturning(JoinPoint joinPoint, Object ret){
        System.out.println("后置通知 : " + joinPoint.getSignature().getName() + " , -->" + ret);
    }

    //拦截@注解的方法
    @AfterReturning(value="PointCutofAnno()",returning = "ret")
    public void myAfterReturningofAnno (JoinPoint joinPoint,Object ret){
        //获取参数
        Object[] objs=joinPoint.getArgs();
        //获取返回值
        Object obj=objs[0];
        Map<String ,Object> inMap= getParameter(obj);
        Map<String ,Object> outMap= getParameter(ret);
        System.out.println(inMap);
        System.out.println(outMap);
    }

    @Before(value="PointCutofAnno()")
    public void myBeforeReturningofAnno (JoinPoint joinPoint){
        //获取参数
        Object[] objs=joinPoint.getArgs();
        //获取返回值
        Object obj=objs[0];
        Map<String ,Object> inMap= getParameter(obj);
        System.out.println(inMap);
    }

    //拓展日志的功能,对拦截的入参进行反射获取信息
    private Map<String, Object> getParameter(Object obj) {
        try {
            //反射对象中的属性
            Class clazz=obj.getClass();
            Field[] fields= clazz.getDeclaredFields();
            Map<String,Object> resultMap=new java.util.HashMap<>();
            //遍历并返回
            for(Field field:fields){
                String fieldName=field.getName();
                PropertyDescriptor pd=new PropertyDescriptor(fieldName,clazz);
                Method readMethod = pd.getReadMethod();
                Object resultObj= readMethod.invoke(obj);
                resultMap.put(fieldName,resultObj);
            }
            return resultMap;
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

运行test代码,最后执行结果如下图所示:

执行结果

源码请fork我的github仓库:https://github.com/tisonkong/aop

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

推荐阅读更多精彩内容