Spring Aspect Oriented Programming

Stella981
• 阅读 813

    本文是一篇Spring AOP的基础知识分析文章,其中不牵扯源码分析,只包含AOP中重要概念的讲解,分析,以及Spring AOP的用法。

    Spring 从2.0版本引入了更加简单却强大的基于xml和AspectJ注解的面向切面的编程方式。在深入了解如何用Spring 进行面向切面的编程前,我们先了解AOP中的几个重要的基本概念,这几个概念并非Spring特有的,并且从字面上看有些难于理解,不过我会尽量用实例和通俗的语言来进行阐述。

    首先,到底什么是AOP呢,它有什么用处呢,对我们程序员有什么好处呢,相信这是所有第一次接触AOP的开发者最想知道的几个问题。我们不妨用一些例子(事务处理或者权限认证等)来稍作解释,通常,在一个应用中我们会为不同的业务模块创建不同的服务类,而大多时候每个服务类中都包含save/remove等业务逻辑不同但应用逻辑相同的接口(此处业务逻辑表示不同的业务需求,而应用逻辑表示增删改查等应用中常见的逻辑)。然而,大多数情况下我们需要对这些方法进行事务控制,比如在所有save/remove 方法执行前开启一个事务,然后方法执行完成后提交事务。这时,如果没有AOP的支持,我们可能就要对于每一个方法都要写一大串重复的毫无兴奋点的代码(当然我们这里暂不提动态代理,其实Spring AOP默认使用动态代理实现)。然而利用AOP我们就可以避免这样的麻烦了,那我该怎样做呢?

    第一步,我们需要定义我们的事务类,该类中包含事务的启用,提交等方法,这个类我们称它为切面(Aspect)。

    第二步,切面定义好了,我们还需要定义其中事务行为,也就是我们需要为当前方法添加的额外行为,我们称之为通知(或者增强)(Advice)。

    第三步,我们需要通过某种定义来决定哪些方法将会被通知(即需要事物处理),我们将这个定义秤为切入点(Pointcut),而每一个被处理的方法我们称之为连接点(Join Point)。

    通过以上几步,我们便可以大致了解了这几个基本概念,切面,通知,切入点,连接点。如果还不明白,你可以这样理解,首先我们需要确定哪些(切入点)方法需要被处理,然后就是对这些方法(连接点)执行哪些额外的代码(通知),以及这些代码在什么时机执行(前置通知。。。), 最后封装通知,切入点的类或接口就是我们的切面)。 另外,对于通知,我们通常分为前置通知,后置通知,环绕通知等等,用于确定通知在连接点执行的何种时机被调用,具体我们下面分析。

    这里要说明下,由于Spring AOP是使用JDK动态代理和CGLIB代理实现的,因此Spring AOP只可以对方法的执行进行拦截,如果需要拦截字段的访问或更新,则需要像AspectJ这样的AOP语言。另外Spring可以无缝的集成IOC,Spring AOP 以及AspectJ AOP。

    上面已经提到Spring AOP提供了基于AspectJ注解和XML两种编程方式,但是这篇文章我们只分析如何给予注解进行编程。

    @AspectJ 注解方式,指的是一种使用Java 注解的方式来进行AOP编程的方式,而这些注解都是AspectJ 项目中引入的,而Spring 可以使用AspectJ库解释这些注解以完成切入点(@Pointcut)的解析和匹配,并且这一切都不需要依赖于AspectJ的编译器和切面编织器。

    为了使用@AspectJ注解,我们需要导入aspectjweaver.jar包,并且启用beans的自动代理,无论这些beans是否被切面拦截,换句话说,自动代理会检测被切面拦截的beans,然后为这些beans自动生成代理以完成对相应方法的增强。我们可以使用XML或者Java代码的方式启用自动代理配置。

<aop:aspectj-autoproxy/>

    Java方式我们不做额外介绍。

    准备工作完成后,我们便可以进行切面编程了:

1,实例

    在一个订单系统,和支付系统中,我们都需要严格的用户身份认证以及日志记录功能,如用户下订单,浏览订单,支付前,都需要判断用户是否登录等,而在下订单,支付完成功通常都需要进行log,或发信通知,而这些功能相对重复,我们可以将它看做一个切面用在任何需要的地方,而不需要repeat yourself。既然需求定下了我们开始编码。

    下面是我们的applicationContext.xml的内容,我们使用注解的方式配置beans,并启用Spring包自动扫描,如果对这个配置,可以阅读我的其他关于spring的文章:

<context:component-scan base-package="aop"/>//包名就aop吧省事
<context:annotation-config/>
<aop:aspectj-autoproxy/>

    下面是我们的OrderService 和PaymentService接口与实现:

package aop;
//interfaces
public interface OrderService {
    public void save();
    public void read();
}
public interface PayService {
    public void save();    
}

//implementations
import org.springframework.stereotype.Component;

@Component("orderService")
public class OrderServiceImpl implements OrderService {
    @Override
    public void save() {
        System.out.println("order saved.");
    }
    @Override
    public void read() {
        System.out.println("order read.");
    }
}

@Component("payService")
public class PayServiceImpl implements PayService {
    @Override
    public void save() {
        System.out.println("pay saved.");
    }
}

    服务接口定义完了,下面我们需要定义我们的切面了,下面是拦截规则,也就是切点Pointcut:

import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
public class Pointcuts {
    @Pointcut("execution(* aop.*.save())")
    public void notice(){}
    
    @Pointcut("execution(* aop.*.*())")
    public void securityCheck(){}
}

    以上定义了两个切点aop.Pointcuts.notice()和aop.Pointcuts.securityCheck(),分别指定了哪些方法需要notice拦截和securitycheck拦截。接下来就是定义对被拦截的方法执行哪些额外操作了,也就是我们的通知。

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class NoticeAspect {
    @AfterReturning(pointcut="aop.Pointcuts.notice()")
    public void notice(){
        System.out.println("Users have been noticed.");
    }
}

    这个类有一个@Aspect 注解,表明该类是一个切面,其中定义的方法有一个@AfterReturning方法,该注解表明相应方法为一个后置通知,它有一个pointcut属性来引用上面定义的切面,确定拦截哪些方法。好了,这样一个再简单不过的切面编程就完成了,我们看下启动方法:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("/application.xml");
        OrderService orderService = (OrderService)context.getBean("orderService");
        orderService.save();
        //orderService.read();
        PayService payService = (PayService)context.getBean("payService");
        payService.save();
    }
}

    最终的运行结果是:

order saved.
Users have been noticed.
pay saved.
Users have been noticed.

    上面的代码可以说是最简单的AOP了吧,下面我们就深入的分析一下,AOP的方方面面。

2,切面(@Aspect)

    Spring AOP中的切面可以看成一个常规的类,该类需要被@Aspect注解,其中可以包含切片,通知等声明,上面的例子中,我们在切面中声明了后置通知。你可以可以将切片声明其中,具体怎样做,还看个人习惯,以及系统组织架构,业务逻辑需要而定了。

3,切片(@Pointcut)

    切片的作用是用来决定我们声明的通知在什么时候被执行。一个切片的生命有两部分组成:1)由名称和任意参数组成的切片签名,2)切片表达式。在Spring AOP 中的注解方式中,切片的签名就是该常规方法定义的签名,如上例的

@Pointcut("execution(* aop.*.save())")//切片表达式
public void notice(){}//切片签名

    注:作为切片签名的方法必须是void返回值。

    Spring AOP目前仅支持部分AspectJ中定义的切片标识符, 下面便是完整的支持列表:

execution - 切片定义的主要用法,用于匹配方法连接点的执行。
within - 将连接点的匹配限定在某个特定类型中
this - 将AOP代理的类型限定在某个特定的类型中
target - 将目标对象的类型限定在某个特定的类型中
args - 限定连接点的参数为某些特定类型
@target - 限定目标对象为具有某个注解的特定类型
@args - 限定连接点的参数类型具有特定的注解
@within - 将连接点的匹配限定在某个具有特定注解的类型中
@annotation - 限定执行该连接点的对象为某个特定的类型

    以下则是Spring AOP还未实现的标识符call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, and @withincode如果无意使用了这些还没支持的切片,则Spring会抛出IllegalArgumentException。

    前面曾提到Spring AOP是基于代理的实现方式,因此我们会用target表示被代理对象(也就是上例的OrderServiceImpl),用this表示代理对象(也就是上例的Spring 为拦截被代理对象所自动创建的对象,详细分析可阅读关于Java动态代理的文章)

    另外,Spring 还支持bean切片,用于将连接点的匹配限定在指定的bean内。用法如下:

bean(idOrNameOfBean)

    其中idOrNameOfBean可以是任意的Spring Bean的名字或者ID,并且支持*通配符,因此如果你的项目遵循好的命名规范,你可以很容易的写出强大的bean切片。

    再编写切片时,我们通常会用到一下几个小技巧:

    A,切片表达式支持%%, || 以及!。

    B,切片表达式支持对切片签名的引用。

    C,我们可以将常用的切片进行统一管理(参考上例)

public class Pointcuts {
        @Pointcut("execution(public * *(..))")//公共方法切片
        public void publicOperation(){}
        
    @Pointcut("within(aop.service..*)")
    public void notice(){}
    
    @Pointcut("publicOperation() && notice()")
    public void noticePublic(){}
}

    通过以上技巧,我们可以组合各种各样复杂的切片。另外需要注意,在引用切片签名时,需要遵循常规的Java方法的可见性约束,如同一个类型中可以访问private 切片定义。以下是一个Spring 提供的可能的企业开发中的切片组织方案:

package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
    //用来匹配web层的切片,匹配位于web及其子包中定义的类中的方法
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}
    //同上,用于匹配service层的切片
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}
    //用来匹配dao层
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}
    //以下切片表达式,假设我们的包结构为
    //com.aop.app.order.service...
    //com.aop.app.pay.service...
    //该切片会匹配这些service包下的所有类的所有方法的执行
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}
    //匹配dao包下的所有方法的执行
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

    这样你就可以在任意地方来引用这些切片定义了。

    Execution 表达式

    execution是最常用的一种切片标识符,我们有必要分析下该切片表达式的格式:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

    有上述表达式可以看出,除了返回值类型(ret-type-pattern),名字(name-pattern),参数(param-pattern),其他部分都是可选的。其中ret-type-pattern决定了连接点的返回值,大多数情况下我们用*通配符类匹配所有的返回值。name-pattern用来匹配方法名称,我们同样可以用*来作为方法名的全部或部分。

对于param-pattern来说:()匹配没有参数的方法,(..)匹配任意数量参数的方法,(*)匹配有一个任意参数的方法,(*, String)匹配两个参数的方法,第一个参数为任意类型,第二个参数为String类型。以下是一些常用的切片表达式:

execution(public * *(..))
execution(* set*(..))
execution(* com.xyz.service.AccountService.*(..))
execution(* com.xyz.service.*.*(..))
execution(* com.xyz.service..*.*(..))
within(com.xyz.service.*)
within(com.xyz.service..*)
this(com.xyz.service.AccountService)
target(com.xyz.service.AccountService)
args(java.io.Serializable)
@target(org.springframework.transaction.annotation.Transactional)
@within(org.springframework.transaction.annotation.Transactional)
bean(*Service)

    我们不一一分析了,相信你能理解这些表达式的意思。

4,通知(@Advice)

    明白了切片,我们再来了解下通知。通常,通知是需要跟切片结合在一起使用,并且会在与切片匹配的方法的前后被执行。而此处的切片既可以是对其它切片的简单引用(通过切片签名),亦可以是一个切片表达式。

    上面已经说过,Spring中支持前置通知,环绕通知,AfterReturning通知,After通知,异常抛出通知,下面我们逐个介绍。

4,1 前置通知(@Before)

@Aspect
public class BeforeAdvice{
    @Before("aop.Pointcuts.notice()")//reference to another pointcut definition.
    public void before(){
        //...
    }
    @Before("execution(* aop.OrderService.*())")
    public void before1(){
        //...
    }
}

    上面的两种前置通知定义方式都是可行的,before与before1两个方法会在匹配的连接点的执行之前被执行。

4,2 AfterReturning通知

@Aspect
public class AfterReturningExample {    
    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {        
        // ...
    }
    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {        
        // ...
    }
}

    上面是AfterReturning通知的两个实例,其中第二个实例中,我们可以通过@AfterReturning注解中的returning属性来访问连接点方法执行后返回的结果。

4,3 AfterThrowing通知

@Aspect
public class AfterThrowingExample {    
    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {        
        // ...
    }
    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {        
        // ...
    }
}

    AfterThrowing 通知会在匹配的连接点方法中抛出异常后执行,并且你可以像第二个实例中那样来捕获连接点中抛出的异常实例。

4,4 After(finally)Advice

@Aspect
public class AfterFinallyExample {    
    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {        
        // ...
    }
}

    After 通知无论连接点方法的执行结果如何都会得到执行。

4,5 Around Advice

    环绕通知顾名思义,会在连接点的前后都被执行,并且可以决定连接点是否被执行,通常环绕通知会被用在连接点执行的前后需要共享数据的场景中。但是Spring建议我们不要一味的使用Around 通知,而是使用能满足你需求的最简单的通知类型。比如Before ,AfterReturning等。

    Around 通知通过@Advice注解声明,并且通知方法的第一个参数必须是ProceedingJoinPoint。然后在方法体内调用该实例的proceed()来调用连接点方法,proceed()也可接受Object[]参数,其中每个元素都作为连接点方法的参数。

@Aspect
public class AroundExample {    
    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {        
        // start stopwatch
        Object retVal = pjp.proceed();        
        // stop stopwatch
        return retVal;
    }
}

4,6 为通知传递参数

    前面提到在AfterReturning通知和AfterThrowing通知中都可以通过参数来访问到返回值或异常实例等,然而有的时候我们可能需要在通知方法中访问连接点方法中的变量,比如我们需要拦截一个含有user参数的方法,并且希望在通知也操作该user实例,那么我们可以这样做:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(user,..)")
public void validate(User user) {    // ...}

//或者
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(user,..)")
private void accountDataAccessOperation(User user) {}

@Before("accountDataAccessOperation(user)")
public void validate(User user) {    // ...}

    args(user, ...)是切片表达式的一部分,它定义了连接点至少应该有一个参数,并且应该是User类型的,另外它可以使得该user实例作为通知方法的参数来使用。

    

    文章到这也就基本结束了,Spring AOP中的常用概念也基本分析了,当然还有很多没有提到,不过哪些已经超出本文的定位了,另外以上内容应该也可以应对日常开发工作中的大部分需求了。

    欢迎讨论,拍砖

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Spring 学习笔记(四):Spring AOP
@\TOC\1概述本文主要讲述了AOP的基本概念以及在Spring中AOP的几种实现方式。2AOPAOP,即AspectOrientedProgramming,面向切面编程,与OOP相辅相成。类似的,在OOP中,以类为程序的基本单元,在AOP中的基本单元是Aspect
Stella981 Stella981
3年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这