Spring两大核心之AOP学习

正文

在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的。比如:日志、声明式事物、安全和缓存。这些东西都不是我们平时写代码的核心功能,但许多地方都要用到。

把这些横切关注点与业务相分离正是面向切面编程(AOP)索要解决的问题。

简单的说就是把这些许多地方都要用到,但又不是核心业务的功能,单独剥离出来封装,通过配置指定要切入到指定的方法中去。

什么是面向切面编程


如上图所示,这就是横切关注点的概念,水平的是核心业务,这些切入的箭头就是我们的横切关注点。
横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:

  • 首先,现在每个关注点都集中于一个地方,而不是分割到多处代码中
  • 其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。

Aop使用场景:

Authentication 权限

Caching 缓存

Context passing 内容传递

Error handling 错误处理

Lazy loading 懒加载

Debugging  调试

logging, tracing, profiling and monitoring 记录跟踪 优化 校准

Performance optimization 性能优化

Persistence  持久化

Resource pooling 资源池

Synchronization 同步

Transactions 事务

定义AOP术语

为了理解AOP,我们必须先了解AOP的相关术语,很简单不难:

通知(Advice)
在AOP中,切面的工作被称为通知。通知定义了切面“是什么”以及“何时”使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。

Spring切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为。

切点(Pointcut):
如果说通知定义了切面“是什么”和“何时”的话,那么切点就定义了“何处”。比如我想把日志引入到某个具体的方法中,这个方法就是所谓的切点。

切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容———他是什么,在何时和何处完成其功能。

引入(Introduction)
引入允许我们向现有的类添加新的方法和属性(Spring提供了一个方法注入的功能)。

织入(Weaving)
把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:

  • 编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器
  • 类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码
  • 运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术

Spring对AOP的支持

创建切入点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了4种类型的AOP支持:

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(使用与Spring各版本)

前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

这里我不准备介绍经典Spring AOP,因为引入了简单的声明式AOP和基于直接的AOP后,Spring经典的AOP看起来就显得非常笨重和过于复杂。

对于新手入门来说,我们不需要知道这么多,在这里我也只介绍2,3两种方式,简单的说就是一个基于xml配置,一个基于注解。

下面就直接开始举两个例子分别来介绍下这两种AOP方式,我们就拿简单的日志来说明。

基于注解的方式

首先基于注解的方式需要引入这些包,对用的pom.xml如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.8</version>
</dependency>

我们还是举前面用到的UserController来说明,下面方法很简单,执行进入这个方法的时候会打印“进来了”信息,现在我打算给这个方法加日志,在执行该方法前打印“进来前”,在执行完方法后执行“进来后”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.tengj.demo.controller;

@Controller
@RequestMapping(value="/test")
public class UserController {
    @Autowired
    UserService userService;
    
    @RequestMapping(value="/view",method = RequestMethod.GET)
    public String index(){
        userService.sayHello("tengj");
        return "index";
    }
}

servie层代码:

1
2
3
4
package com.tengj.demo.service
public interface UserService {
    public void sayHello(String name);
}

servie实现类代码:

1
2
3
4
5
6
7
8
package com.tengj.demo.service.impl;
@Service("userService")
public class UserServiceImpl implements UserService{
    @Override
    public void sayHello(String name) {
        System.out.println("hello,"+name);
    }
}

 

上面方法index()其实就是我们之前定义的切点,表示在哪里切入AOP。

如图所示,我们使用execution()指示器选择UserServiceImpl的sayHello方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的sayHello()方法,无论该方法的入参是什么。

接下来我们要定义个切面,也就是所谓的日志功能的类。

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
29
30
package com.tengj.demo.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component //注入依赖
@Aspect //该注解标示该类为切面类
public class LogAspect {
    @Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
    public void logAop(){}

    @Before("logAop() && args(name)")
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    @AfterReturning("logAop()")
    public void logAfterReturning(){
        System.out.println("返回通知AfterReturning");
    }

    @After("logAop() && args(name)")
    public void logAfter(String name){
        System.out.println(name+"后置通知After");
    }

    @AfterThrowing("logAop()")
    public void logAfterThrow(){
        System.out.println("异常通知AfterThrowing");
    }
}

上面就是切面类的代码,很简单,这里用到了前面提的通知的几种类型。
这样就能实现切入功能了

1
2
@Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}

这里的@Pointcut注解是为了定义切面内重用的切点,也就是说把公共的东西抽出来,定义了任意的方法名称logAop,这样下面用到的各种类型通知就只要写成

1
2
3
@Before("logAop() && args(name)")
@AfterReturning("logAop()")
@AfterThrowing("logAop()")

这样既可,否则就要写成

1
2
3
@Before("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterReturning("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterThrowing("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")

大家是否注意到了@Before("logAop() && args(name)")这里多出来个&& args(name),这个是用来传递参数的,定义只要跟sayHello参数名称一样就可以。

如果就此止步的话,LogAspect只会是Spring容器中的一个Bean,即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

所以需要在XML里面配置一下,需要使用Spring aop命名空间中的<aop:aspectj-autoproxy/>元素,简单如下:

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
<?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:mvc="http://www.springframework.org/schema/mvc"
       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.1.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.1.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd"
       default-lazy-init="true">
    <context:component-scan base-package="com.tengj.demo"/>
    <mvc:resources location="/WEB-INF/pages/" mapping="/pages/**"/>
    <!-- 默认的注解映射的支持 -->
    <mvc:annotation-driven/>
    <!--启用AspectJ自动代理-->
    <aop:aspectj-autoproxy/>
    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

 

接着就可以启动工程,访问index这个方法,http://localhost:8080/SpringMVCMybatis/test/view
执行结果:

1
2
3
4
tengj前置通知Before
hello,tengj
tengj后置通知After
返回通知AfterReturning

 

根据前面学的我们知道,除了上面提到的通知外,还有一个更强大通知类型,就是环绕通知。可以自定义我们需要切入的位置,可以替代上面提到的所有通知。看例子:

1
2
3
4
5
6
7
8
9
10
11
@Around("logAop()")
public void logAround(ProceedingJoinPoint jp){
    try {
        System.out.println("自定义前置通知Before");
        jp.proceed();//将控制权交给被通知的方法,也就是执行sayHello方法
        System.out.println("自定义后置通知After");
    } catch (Throwable throwable) {
        System.out.println("异常处理~");
        throwable.printStackTrace();
    }
}

 

执行结果:

1
2
3
自定义前置通知Before
hello,tengj
自定义后置通知After

 

这里主要是通过ProceedingJoinPoint这个参数。其中里面的proceed()方法就是将控制权交给被通知的方法。如果你忘记调用这个方法,那么你的通知实际上会阻塞对被通知方法的调用。

有意思的是,你可以不调用proceed()方法,从而阻塞堆被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

基于XML配置的方式

这里介绍使用XML配置的方式来实现,在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面。

AOP配置元素用 途
<aop:advisor>定义AOP通知器
<aop:after>定义AOP后置通知(不管被通知的方法是否执行成功)
<aop:after-returning>定义AOP返回通知
<aop:after-throwing>定义AOP异常通知
<aop:around>定义AOP环绕通知
<aop:aspect>定义一个切面
<aop:aspectj-autoproxy>启用@AspectJ注解驱动的切面
<aop:before>定义一个AOP前置通知
<aop:config>顶层的AOP配置元素,大多数的<aop:*>元素必须包含在<aop:config>元素内
<aop:declare-parents>以透明的方式为被通知的对象引入额外的接口
<aop:pointcut>定义一个切点

我们已经看过了<aop:aspectj-autoproxy/>元素,它能够自动代理AspectJ注解的通知类。aop命名空间的其他元素能够让我们直接在Spring配置中声明切面,而不需要使用注解。
所以,我们重新来看看一下这个LogAspect类,这次我们将它所有的AspectJ注解全部移除掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tengj.demo.aspect;

public class LogAspect {
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    public void logAfterReturning(String name){
        System.out.println("返回通知AfterReturning");
    }

    public void logAfter(String name){
        System.out.println(name+"后置通知After");
    }

    public void logAfterThrow(String name){
        System.out.println("异常通知AfterThrowing");
    }
}

 

然后在xml配置文件中使用Spring aop命名空间中的一些元素,详细基本配置参考上面注解方式中的xml配置,这里是贴出来关键的代码:

1
2
3
4
5
6
7
8
9
10
11
<bean id="logAspect" class="com.tengj.demo.aspect.LogAspect" />
<aop:config>
        <aop:aspect id="log"  ref="logAspect">
            <aop:pointcut id="logAop" expression="execution(* com.tengj.demo.service.impl.UserServiceImpl.sayHello(..)) and args(name)"/>
            <aop:before method="logBefore" pointcut-ref="logAop"/>
            <aop:after method="logAfter"  pointcut-ref="logAop"/>
            <aop:after-returning method="logAfterReturning"  pointcut-ref="logAop"/>
            <aop:after-throwing method="logAfterThrow" pointcut-ref="logAop"/>
            <!--<aop:around method="logAfterThrow"  pointcut-ref="logAop"/>-->
        </aop:aspect>
</aop:config>

 

配置也 很好理解

  • xml里面配置aop,都是放在<aop:config>里面
  • 然后使用<aop:aspect>一个切面,指向具体的bean类。
  • 使用<aop:pointcut>定义切点,基本跟注解的很像,其中要注意的是xml配置里面如果要带参数的,用的不再是&&,要使用and关键字才行(因为在XML中,“&”符号会被解析为实体的开始)
  • 然后就是使用各种通知标签了,简单。

执行效果如下:

1
2
3
4
tengj前置通知Before
hello,tengj
tengj后置通知After
返回通知AfterReturning

 

环绕通知也很简单,直接贴代码:
xml配置:

1
<aop:around method="logAround"  pointcut-ref="logAop"/>

 

切面方法:

1
2
3
4
5
6
7
8
9
10
public void logAround(ProceedingJoinPoint jp,String name){
    try {
        System.out.println(name+"自定义前置通知Before");
        jp.proceed();
        System.out.println(name+"自定义后置通知After");
    } catch (Throwable throwable) {
        System.out.println("异常处理~");
        throwable.printStackTrace();
    }
}

 

执行结果:

1
2
3
tengj自定义前置通知Before
hello,tengj
tengj自定义后置通知After
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值