Spring学习笔记(六)——AOP篇(上)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Mr_Megamind/article/details/81118698

前提

这篇博文是这套Spring学习笔记的第六篇——AOP篇(上),主要内容包含Spring AOP的基础知识及应用,全篇以一个我遇到的真实编程问题的出现、思考及通过Spring AOP得以解决的过程,使大家对AOP的应用场景可以有一个深刻的认识。如果需要了解有关Spring的综述信息或博文的索引信息,请移步:
《综述篇》


什么是AOP?

先从概念上来说,AOP即Aspect Oriented Programing——面向切面的编程,类似的一个概念即OOP(Object Oriented Programing)——面向对象的编程。AOP并不是用来取代OOP的,而是应OOP中某些特定的应用场景而诞生的。

具体的应用场景以下面的例子给出。
《配置篇》的“controller包和UserController”这一小节,我们创建了一个UserController类,其中有个handleLoginRequest函数用来执行用户登录的业务逻辑,我们再给它添上一个Register注册函数,代码如下:

@Controller  
public class UserController {

    @Autowired  
    private UserService userService;

    @RequestMapping("Login")  
    private void handleLoginRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try (PrintWriter out = response.getWriter()) {
            request.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            response.setCharacterEncoding("utf-8");
            //此处登录功能的业务逻辑省略
        }
    }
    
    @RequestMapping("Register")  
    private void handleRegisterRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try (PrintWriter out = response.getWriter()) {
            request.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            response.setCharacterEncoding("utf-8");
            //此处注册功能的业务逻辑省略
        }
    }
}

可以看到两个函中有三行重复代码,它们的作用是设置请求和响应中的字符编码:

request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");

我们可以想象到的是,在工程的规模逐渐扩大后,这种请求处理函数会有很多,如果每个函数前面都有这么三行重复代码,很明显违反了OOP的原则。 那么一般想法就是给这三行代码提到一个函数中去,每个处理请求的函数再来调用这个函数。但是这样做只是把重复的三行代码变成了一行,并不算是“优雅”地解决了这个问题。

我们设想能有一种类似函数监听器的东西,能在我们指定的函数执行之前先执行这三行代码。这样我们就不用在业务逻辑中见到它们了。

Spring AOP就是用来实现类似的功能的,除了上述场景,AOP主要适用于性能监控、日志记录等与业务逻辑关系不大的场景中。在传统的OOP中,一般这类代码必须加在主要业务逻辑的一端或两端,通过AOP可以在保证功能的前提下对这些代码和业务逻辑进行解耦。


AOP的基础术语

①连接点(JoinPoint)
准确的说这个概念更贴近于“时间上的点”,而不是“代码顺序上的点”。比如函数执行前、执行后和抛出异常时等。

②切点(Pointcut)
切点是连接点的子概念,一个函数可以有多个连接点,但是我们关心的,要对其执行操作的连接点才是切点。比如上述例子中,处理请求的各函数的“执行前”这个连接点才是切点,其他的连接点我们不关心,也就不是切点。

③增强(Advice)
即我们在切点上要做的操作。比如上述例子中设置请求和响应中的字符编码的那三行代码。

④织入(Weaving)
即AOP将增强和原代码中的业务逻辑结合起来的过程。

⑤切面(Aspect)
它是切点和增强概念的结合,Spring AOP把切面定义的增强编织到切面定义的切点中。通俗的来说,就是Spring AOP会根据我们的命令在函数运行的特定的点上执行特定的操作。


增强的类型

Spring AOP支持五种增强类型:前置增强、后置增强、环绕增强、异常抛出增强和引介增强。

扫描二维码关注公众号,回复: 3478569 查看本文章

①前置增强(Before):即在函数调用前施以增强;
②后置增强(AfterReturning):即在函数返回后施以增强;
③环绕增强(Around):即在函数执行前后都施以增强;
④异常抛出增强(AfterThrowing):即在函数抛出异常后施以增强;
⑤引介增强(Introduction):不同于其他增强,引介增强可以对指定的类在运行时动态的实现指定的接口。


基于AspectJ的AOP

AspectJ是一个AOP框架,Spring无缝集成了AspectJ。

注意:使用AspectJ前需要额外添加aspectjtools和cglib的Jar包到库中。
aspectjtools:链接 密码:du9t
cglib:链接 密码:7tva

我们先给出上述问题的实现代码,首先是切面类:

@Aspect  //注释①
public class BeforeHandleRequestAspect{
    @Before("execution(* com.implementist.MyFirstWebApp.controller.*(..))")  //注释②
    public void beforeHandleRequest(JoinPoint joinPoint){  //注释③
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = (HttpServletRequest) args[0];
        HttpServletResponse response = (HttpServletResponse) args[1];
        
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        response.setCharacterEncoding("utf-8");
    }
}

注释:
①@Aspect注解标示该类是一个切面类;
②@Before注解标示这是一个前置增强,括号里的字符串是一个切点表达式,作用是定位com.implementist.MyFirstWebApp.controller包下的所有类中的所有函数。这一行的整体效果是对上述所有函数织入前置增强;
③增强的函数体,即前置增强期间要做的操作。通过定义JoinPoint参数可以访问切点的信息,如参数列表等。

接着需要对spring-mvc.xml文件做两部分修改:
①为<beans>标签增加aop命名空间的声明:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/context/spring-context-4.3.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
">

②因为作为被增强的对象,controller包下面的类在经历扫描后已经生成了对应的bean,所以还需要添加前置增强切面类的bean和aop自动代理的代码:

<bean class="com.implementist.MyFirstWebApp.BeforeHandleRequestAspect"/>
<aop:aspectj-autoproxy/>

这样,Spring AOP就会自动在指定的函数切点上织入前置增强,每当函数执行前,都会先执行增强函数来设置request和response的字符编码。

切点表达式函数

在上述代码中,我们看到
@Before("execution(* com.implementist.MyFirstWebApp.controller.*(..))")
这个前置增强注解的参数好像有一个用字符串定义的函数
execution(* com.implementist.MyFirstWebApp.controller.*(..))
这个execution()就是Spring支持的9个切点函数之一,另外8个函数是:@annotation()、args()、@args()、within()、target()、@within()、@target()和this()。

通配符

上述函数中,有的可以使用通配符,AspectJ一共支持三种通配符:
①*匹配单个任意字符串;
②…匹配多个任意字符串,表示类时必须与*连用,即…*;
③+按类型匹配,如execute(* Animal+(..))匹配Animal类及其子类的所有函数。

注意:
①execution()和within()支持全部通配符;
②args()、this()和target()仅支持+通配符;
③其余的函数不支持任何通配符。

逻辑运算符

和Java中的逻辑运算符相似,上述9个切点表达式函数可以通过逻辑运算符进行逻辑运算,逻辑运算符即①&&表示与;②||表示或;③!表示非
如我需要对controller包下所有类的所有函数中,参数为request和response的函数织入后置增强:

@AfterReturning("within(com.implementist.MyFirstWebApp.controller.*) and args(request,response)")

各增强对应的注解

前置增强——@Before
后置增强——@AfterReturning
环绕增强——@Around
异常抛出增强——@AfterThrowing
引介增强——@DeclareParents
另外,AspectJ还支持一个Final增强——@After,这个增强相当于异常捕获模块的final块,无论程序正常还是异常退出,都会执行该增强。

各切点表达式函数的使用方法

①execution:最常用的函数,其语法为:
execution(<修饰符>?<返回类型><函数名>(<参数>)<异常>?)
如 execution(public * com…*.*Function(String,int,…)) 表示权限为public的com包下的所有类中,名称以“Function”为后缀的,前两个参数分别是String和int型,后面可以有任意多个参数的函数。

注意:在java.lang包下的类直接写非全限定名即可,如Stringintdouble等;除此之外的类都要写全限定名,如com.implementist.MyFirstApp.domain.User

②@annotation:表示标注了指定注解的全部函数,参数为指定注解的全限定名,如
@annotation(com.implementist.MyFirstWebApp.ContainsBugs)表示所有被冠以@ContainsBugs注解的函数。

③args:表示参数是指定的类型的函数,如
args(String,int)表示仅有两个参数,且分别为String和int类型的全部函数(顺序必须一致);
args(String,int,*)表示仅有三个参数,前两个参数分别为String和int类型,第三个参数为任意类型的全部函数;
args(String,int,..)表示前两个参数分别为String和int类型,后面可以有任意个任意类型参数的全部函数。

④@args:表示参数被冠以指定类型注解的函数,如
@args(com.implementist.MyFirstWebApp.ContainsBugs)表示仅有一个参数,且该参数被冠以了@ContainsBugs注解的全部函数。

注意:因为注解是可以被继承的,在A◁——B◁——C这样一个类继承树上:
①如果A中有函数被冠以@ContainsBugs注解,在B和C中用@args(com.implementist.MyFirstWebApp.ContainsBugs)做切点匹配不到任何连接点,因为注解点A高于判断点B或C;
②反之,在C中有函数被冠以@ContainsBugs注解,在A和B中用@args(com.implementist.MyFirstWebApp.ContainsBugs)做切点都可以匹配到C,因为注解点C低于判断点A或B。

⑤within:表示类匹配模式,它可以匹配指定类及其子类中的全部函数,它是静态植入的,即在程序运行前, 参数最低只能指定到类级别,如
within(com.implementist.MyFirstWebApp.controller.*)表示com.implementist.MyFirstWebApp.controller包下的所有类及其子类的所有函数。

⑥target:表示类匹配模式,它可以匹配指定类及其子类中的全部函数,它是动态植入的,即在程序运行期间, 参数最低只能指定到类级别,如
target(com.implementist.MyFirstWebApp.Utils)表示com.implementist.MyFirstWebApp.Utils及其子类的全部函数。

注意,如果此时通过引介增强等手段为Utils类或者其子类实现了额外的接口比如Collection接口,那么其中的size()等函数也会被匹配为切点。这边是target动态植入与within静态植入的不同。

⑦@within:表示被冠以指定注解的类及其子类的全部函数,如
@within(com.implementist.MyFirstWebApp.ContainsBugs)表示被冠以@ContainsBugs注解的类及其子类的全部函数。

⑧@target:表示匹配被冠以指定注解的类的所有函数,如
@within(com.implementist.MyFirstWebApp.ContainsBugs)表示被冠以@ContainsBugs注解的类的全部函数。

⑨this:表示代理类匹配模式,它可以匹配指定类及其子类中的全部函数,它是动态植入的,即在程序运行期间,一般情况下它的效果和target()是等效的

注意,它与target()的不同点在于,当通过引介增强为目标类动态实现一个接口时,该接口内的函数也会被全部匹配。


后记

这一篇就先到这了,因为AOP这里的知识确实多,所以我决定分为上下两篇,一可以让读者休息一下,不至于一口气看的太累。二来我也可以先整体浏览一下这前半部分,然后休息一下接着写。

另外,有一些在AOP发展过程中很重要,曾经需要我们手动添加但现在已经被封装的一些东西我就省略掉了,比如最后只提到了一下的代理这个东西。虽然在AOP中可以自动代理,但是代理这个概念还是很重要的,比如设计模式中就有一个代理模式,有兴趣的话可以移步《Java设计模式之代理模式(Proxy)》

猜你喜欢

转载自blog.csdn.net/Mr_Megamind/article/details/81118698