Spring学习笔记(三): Spring 面向切面

面向切面编程是 Spring 最为重要的功能之一,在数据库事务管理中广泛应用。

全部章节传送门:
Spring学习笔记(一):Spring IoC 容器
Spring学习笔记(二):Spring Bean 装配
Spring学习笔记(三): Spring 面向切面
Spring学习笔记(四): Spring 数据库编程
Spring学习笔记(五): Spring 事务管理

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

AOP 常用术语

spring-aop.jpg
  • 切面(Aspect): 通常是一个类,里面可以定义切入点和通知。
  • 通知(Advice): 通知是切面开启后,切面的方法,它根据在代理对象真实方法调用前、后的顺序和逻辑区分,一共五类:
    • 前置通知(before): 在动态代理反射原有对象或者执行环绕通知前执行的通知功能。
    • 后置通知(after): 在动态代理反射原有对象或者执行环绕通知后执行的通知功能,无论是否抛出异常,它都会被执行。
    • 返回通知(afterReturning): 在动态代理反射原有对象或者执行环绕通知后正常返回(无异常)执行的通知功能。
    • 异常通知(afterThrowing): 在动态代理反射原有对象或者执行环绕通知产生异常后执行的通知功能。
    • 环绕通知(around): 在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。
  • 引入(Introduction): 在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
  • 连接点(join point): 被拦截到的点。
  • 切点(Pointcut): 对连接点进行拦截的定义。
  • 织入(Weaving): 将切面应用到目标对象并导致代理对象创建的过程。

Spring 对 AOP 的支持

Spring AOP 是一种基于方法拦截的 AOP,它只支持方法拦截。在 Spring 中有4种方法去实现 AOP 的拦截功能。

  • 使用 ProxyFactoryBean 和对应的接口实现 AOP。
  • 使用 XML 配置 AOP。
  • 使用 @AspectJ 注解驱动切面。
  • 使用 AspectJ 注入切面。
    其中,真正常用的是 @AspectJ 注解,有时候 XML 配置也有一定的辅助作用,另外2种很少使用。

使用 @AspectJ 注解开发 Spring AOP

使用 @AspectJ 注解的方式是当下的主流方式。

创建一个 maven 项目, 引入 Spring 相关依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

选择连接点

创建一个接口,使用其中的方法作为连接点。

package com.wyk.aopdemo.service;

import com.wyk.aopdemo.domain.Role;

public interface RoleService {
    public void printRole(Role role);
}

然后编写它的实现类。

package com.wyk.aopdemo.service.impl;

import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.stereotype.Component;

@Component
public class RoleServiceImpl implements RoleService {
    public void printRole(Role role) {
        System.out.println("{id: " + role.getId() + ", role_name: "
            + role.getRoleName() + ", note: " + role.getNote() + "}");
    }
}

其中的 Role 是一个实体类。

package com.wyk.aopdemo.domain;

public class Role {
    private Long id;
    private String roleName;
    private String note;

    public Long getId() {
        return id;
    }

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

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

创建切面和切点

在类上添加 @Aspect 注解即可将其变成一个切面。

package com.wyk.aopdemo.aspect;

import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @Pointcut("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))")
    public void print() {

    }
    @Before("print()")
    public void before() {
        System.out.println("before ...");
    }

    @After("print()")
    public void after() {
        System.out.println("after ...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("afterReturn ...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("afterThrowing ...");
    }
}

其中的 @Pointcut 注解用来定义切点,其他注解代表通知,很好理解。如过不定义切点则需要在通知上直接写正则表达式,比较麻烦。

对切点中的正则表达式简单分析一下。

execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))
  • execution: 代表执行方法的时候会触发。
  • *:代表任意返回类型的方法。
  • com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole: 代表类的全限定名。
  • (..): 任意的参数。

生成 AOP 实例

最后需要添加方法配置 Spring 的 Bean 。

package com.wyk.aopdemo.config;

import com.wyk.aopdemo.aspect.RoleAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.wyk.aopdemo")
public class AppConfig {
    @Bean
    public RoleAspect getRoleAspect() {
        return new RoleAspect();
    }
}

其中 @EnableAspectJAutoProxy 注解用来启用 AspectJ 框架的自动代理,这个时候 Spring 会生成动态代理对象,进而可以使用 AOP , 而其中的方法用来生成一个切面实例。

进行测试

编写一个测试类进行测试。

package com.wyk.aopdemo;

import com.wyk.aopdemo.config.AppConfig;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("wyk");
        role.setNote("haha");
        roleService.printRole(role);
        System.out.println("############################");
        //测试异常通知
        role = null;
        roleService.printRole(role);
    }
}

运行程序,在控制台查看结果。很显然切面的通知已经通过 AOP 织入约定的流程当中。

before ...
{id: 1, role_name: wyk, note: haha}
after ...
afterReturn ...
############################
before ...
after ...
afterThrowing ...

环绕通知

环绕通知是 Spring AOP 当中最强大的 AOP ,它可以同时实现前置通知和后置通知。它保留了调度被代理对象原有方法的功能,所以它既强大又灵活,但是可控性不强,如果不需要大量改变业务逻辑,一般而言并不需要使用它。

在 RoleAspect 类中添加环绕通知方法。

@Around("print()")
public void around(ProceedingJoinPoint jp) {
    System.out.println("around before ...");
    try {
        jp.proceed();
    } catch (Throwable e) {
        e.printStackTrace();
    }
    System.out.println("around after ...");
}

在一个切面里通过 @Around 注解加入环绕通知,其中方法中有一个 ProceedingJoinPoint 参数,它由 Spring 提供,可以反射连接点方法。

运行程序,查看结果。

around before ...
before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after ...
afterReturn ...
############################
around before ...
before ...
...(异常堆栈信息)
around after ...
after ...
afterReturn ...

根据控制台输出,可以看到执行顺序。

织入

织入是生成代理对象并把切面内容放入约定流程的过程,上述示例中连接点所在的类都是拥有接口的类。事实上即使没有接口,Spring 也能提供 AOP 功能,但是在 Spring 中建议使用接口编程,因为这样的好处是使定义和实现分离,有利于变化和替换,更加灵活。

给通知传递参数

如果需要给通知传递参数的话,则需要修改通知方法。首先修改连接点为一个多参数的方法。

public void printRole(Role role, int sort) {
    System.out.println("{id: " + role.getId() + ", role_name: "
        + role.getRoleName() + ", note: " + role.getNote() + "}");
    System.out.println(sort);
}

这样在通知中应该如下定义方法。

@Before("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..)) && args(role, sort)")
public void before(Role role, Sort sort) {
    System.out.println("before ...");
}

引入

有时候,我们希望在流程中引入其它类的方法来得到更好的实现,这时需要使用 Spring 的引入技术。

首先新建一个接口,用来检查角色是否为空。

package com.wyk.aopdemo.service;

import com.wyk.aopdemo.domain.Role;

public interface RoleVerifier {
    public boolean verify(Role role);
}

编写其实现类。

package com.wyk.aopdemo.service.impl;

import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleVerifier;
import org.springframework.stereotype.Component;

public class RoleVerifierImpl implements RoleVerifier {
    public boolean verify(Role role) {
        return role != null;
    }
}

在切面 RoleAspect 类中添加一个新属性,用来将 RoleVerifier 加入到切面之中。

@DeclareParents(value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+",
        defaultImpl = RoleVerifierImpl.class)
public RoleVerifier roleVerifier;

注解 @DeclareParents 用来引入类。

  • value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+" 表示对 RoleServiceImpl 类进行增强,也就是在 RoleServiceImpl 中添加一个新的接口。
  • defaultImpl 代表默认的实现类。

修改测试方法,进行测试。

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
        RoleVerifier roleVerifier = (RoleVerifier)roleService;
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("wyk");
        role.setNote("haha");
        if(roleVerifier.verify(role)) {
            roleService.printRole(role);
        }
    }
}

此方法的原理是让代理对象挂到 RoleService 和 RoleVerifier 两个接口之下,这样就可以在它们之间进行强制转换。

使用 XML 配置开发 Spring AOP

使用 XML 方式开发 AOP 和使用注解的原理相同。

接口和接口实现类和上面的相同,这里不在描述。切面类和前面的例子也相同,只是不需要添加注解。

package com.wyk.xmlaop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

public class XmlAspect {
    public void before() {
        System.out.println("before ...");
    }

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

    public void afterThrowing() {
        System.out.println("after-throwing ...");
    }

    public void afterReturning() {
        System.out.println("after-returning ...");
    }

    public void around(ProceedingJoinPoint jp) {
        System.out.println("around before ...");
        try {
            jp.proceed();
        } catch (Throwable e) {
            new RuntimeException("回调原有流程,产生异常 ...");
        }
        System.out.println("around after ...");
    }
}

添加通知

在类路径下面添加 XML 配置文件 spring-cfg.xml 。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-4.0.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
           ">
    <bean id="xmlAspect" class="com.wyk.xmlaop.aspect.XmlAspect" />
    <bean id="roleService" class="com.wyk.xmlaop.service.impl.RoleServiceImpl" />
    <aop:config>
        <!-- 引用xmlAspect作为切面 -->
        <aop:aspect ref="xmlAspect">
            <!--定义切点-->
            <aop:pointcut id="printRole" expression="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..))" />
            <!--定义通知,引入切点-->
            <aop:before method="before" pointcut-ref="printRole" />
            <aop:after method="after" pointcut-ref="printRole" />
            <aop:after-throwing method="afterThrowing" pointcut-ref="printRole" />
            <aop:after-returning method="afterReturning" pointcut-ref="printRole" />
            <aop:around method="around" pointcut-ref="printRole" />
        </aop:aspect>
    </aop:config>
</beans>

在文件中通过<aop:config>定义 AOP 的内容信息。

  • <aop:aspect> 定义切面类。
  • <aop:before> 定义前置通知。
  • <aop:after> 定义后置通知。
  • <aop:after-throwing> 定义异常通知。
  • <aop:after-returning> 定义返回通知。
  • <aop:around> 定义环绕通知。

进行测试

创建测试类进行测试。

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("wyk");
        role.setNote("haha");
        roleService.printRole(role);
    }
}

运行程序,查看结果。

before ...
around before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after-returning ...
after ...

这里的顺序貌似和注解版不一样,不知道为啥。。。

给通知添加参数

修改 before 方法。

public void before(Role role) {
    System.out.println("before ...");
}

然后 XML 中的配置也要修改。

<aop:before method="before" pointcut="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..)) and aegs(role)" />

和注解版不同的是这里使用 and 而不是 && ,这是因为 & 是 xml 保留字符。

引入

引入方法和上面的例子相同,这里不再赘述,其中 XML 需要添加如下配置。

<aop:declare-parents type-matching="com.wyk.xmlaop.service.impl.RoleServiceImpl+"
    implement-interface="com.wyk.xmlaop.service.RoleVerifier"
    default-impl="com.wyk.xmlaop.service.RoleVerifierImpl" />

多个切面

Spring 中也能支持一个连接点包含多个切面,但多个切面之间的顺序是随机的,可以在切面类上使用 @Order注解进行排序。如果是 XML 配置文件,也可以在配置标签上添加 order 属性。

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