目录
二、Spring中AOP的实现——ProxyFactoryBean方式
一. AOP概念
1. AOP主要的功能
主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是设计模式的延续,设计模式是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP可以说也是这种目标的一种实现。
在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
2. AOP术语:
横切关注点(Cross-cutting Concerns) : 在项目的例子中,记录的动作原先被横切(Cross-cutting)入至 HelloProxy本身所负责的业务流程之中,另外类似于记录这类的动作,如安全(Security)检查、事务(Transaction)等系统层面的服务(Service),在一些应用程序之中常被见到安插至各个对象的处理流程之中,这些动作在AOP的术语中被称之为Cross-cutting concerns
切面(Aspect): 将散落于各个业务对象之中的 Cross-cutting concerns收集起来,设计各个独立可重用的对象,这些对象称之为Aspect,例如在项目中将记录日志的动作设计为一个HelloProxy类, HelloProxy类在AOP的术语就是Aspect的一个具体实例
可以这样理解:软件各模块需要嵌入共同的功能,由于各模块均需嵌入这个功能,就好像这个功能是一个切面“切入”各模块。
连接点(Joinpoint)when: 在程序执行过程中某个特定的时间点,比如某方法调用的时候 在Spring AOP中,一个连接点总是代表一个方法的执行的某个时机,比如方法执行前,方法执行后、方法执行发生异常的时候
通知(Advice) what: 在切面的某个特定的连接点上执行的动作,即切面功能的实现。通知有各种类型,其中包括around、before和after等。
-
前置通知(Before advice): 在某连接点之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)。
-
后置通知(After returning advice): 在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
-
异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。
-
最终通知(After (finally) advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
-
环绕通知:在连接点之前执行的通知,但是可以控制是否执行连接点
切入点(Pointcut)where(是一个匹配说明): 通知应用于连接点的匹配说明,即匹配连接点的断言。通俗的说,就是将切面引用项目中哪些类的哪些方法,那么这些方法就称为切入点
目标对象(Target Object): 被一个或者多个切面所通知的对象。也有人把它叫做被通知对象。即应用通知的目标对象。
AOP代理(AOP Proxy): AOP框架创建的代理对象,其作用是代理目标对象,实现切面功能。
织入( Weaving ):Advice被应用至对象之上的过程称之为织入(Weave) ,在AOP中织入的方式有几个时间点。
-
编译期:切面在目标对象编译时织入.这需要一个特殊的编译器.
-
类装载期:切面在目标对象被载入JVM时织入.这需要一个特殊的类载入器.
-
运行期:切面在应用系统运行时织入.
3. Spring 通知的 5 种类型
名称 | 说明 |
---|---|
org.springframework.aop.MethodBeforeAdvice(前置通知) | 在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。 |
org.springframework.aop.AfterReturningAdvice(后置通知) | 在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。 |
org.aopalliance.intercept.MethodInterceptor(环绕通知) | 在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。 |
org.springframework.aop.ThrowsAdvice(异常通知) | 在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。 |
org.springframework.aop.IntroductionInterceptor(引介通知) | 在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)。 |
4. Spring AOP 是基于动态代理模式实现
Spring AOP 是基于动态代理模式实现,采用两种,JDK动态代理、CGLIB的动态代理。
使用 JDK 的 Proxy 实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现。对于无接口的类,要为其创建动态代理,就要使用 CGLIB 来实现。
CGLIB 代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以使用 CGLIB 生成动态代理,要求目标类必须能够被继承,即不能是 final 的类。
二、Spring中AOP的实现——ProxyFactoryBean方式
Spring中的 AOP 代理默认就是使用 JDK 动态代理的方式来实现的
Spring 创建一个 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,并可以生成指定的内容。
1、ProxyFactoryBean 的常用属性
属性名称 | 描 述 |
---|---|
target | 代理的目标对象 |
proxyInterfaces | 代理要实现的接口,如果有多个接口,则可以使用以下格式赋值:<list> <value ></value></list> |
proxyTargetClass | 是否对类代理而不是接口,设置为 true 时,使用 CGLIB 代理 |
interceptorNames | 需要植入目标的 Advice |
singleton | 返回的代理是否为单例,默认为 true(返回单实例) |
optimize | 当设置为 true 时,强制使用 CGLIB |
2、在pom.xml中添加依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring.version>5.3.10</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<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-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
3、编写接口、实现类:
public interface ISpeaker {
void say();
}
public class HelloSpeaker implements ISpeaker {
@Override
public void say() {
System.out.println("hello speaker");
}
}
4、编写切面类:
在 Spring 通知中,环绕通知是一个非常典型的应用。下面通过环绕通知的案例演示 Spring 创建 AOP 代理的过程
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyAspect implements MethodInterceptor {
private static final Logger logger = Logger.getLogger(MyAspect.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
logger.info("日志开始");
Object obj=invocation.proceed();
logger.info("日志结束");
return obj;
}
}
配置文件
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 通知 advice -->
<bean id="myAspect" class="com.XXX.ssm.aop.MyAspect"/>
<!--目标对象 -->
<bean id="speaker" class="com.XXX.ssm.aop.HelloSpeaker"/>
<!--生成代理对象 -->
<bean id="speakerProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理实现的接口 -->
<property name="proxyInterfaces" value="com.XXX.ssm.aop.ISpeaker" />
<!--代理的目标对象 -->
<property name="target" ref="speaker" />
<!--用通知增强目标 -->
<property name="interceptorNames" value="myAspect" />
<!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 -->
<property name="proxyTargetClass" value="true" />
</bean>
ProxyFactoryBean为工厂bean,返回的是代理的对象
注意:target对应的值为ref引用,其它的几个为value赋值
测试代码
public class SSMApplication {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
ISpeaker speaker = ac.getBean("speakerProxy",ISpeaker.class);
speaker.say();
}
}
三、AspectJ开发(面向切面的框架开发)
使用AspectJ实现AOP有两种方式:
-
一种是基于XML的声明式AspectJ
-
另外一种是基于注解的声明式AspectJ
1、pom.xml引入依赖
基于XML的声明式AspectJ在使用AspectJ框架之前先要在pom.xml文件中导入spring-aspects的相关依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
切面类接口和实现类同上
2、编写切面
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Before;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.util.Arrays;
public class LogAspect {
Logger log = Logger.getLogger(LogAspect.class);
//进入方法时间戳
private Long startTime;
//方法结束时间戳(计时)
private Long endTime;
public LogAspect() {
}
//前置通知,方法之前执行
public void doBefore(JoinPoint joinPoint) {
startTime = System.currentTimeMillis();
log.info("请求开始时间:" + LocalDateTime.now());
log.info("请求参数 : " + Arrays.toString(joinPoint.getArgs()));
}
//后置通知
public void doAfter(JoinPoint joinPoint) {
log.info("Logger-->后置通知,方法名:" + joinPoint.getSignature().getName() + ",方法执行完毕");
}
//返回通知 正常结束时进入此方法
public void doAfterReturning(Object ret) {
endTime = System.currentTimeMillis();
log.info("请求结束时间 : " + LocalDateTime.now());
log.info("请求耗时 : " + (endTime - startTime));
// 处理完请求,返回内容
log.info("请求返回 : " + ret);
}
//异常通知: 在目标方法非正常结束,发生异常或者抛出异常时执行
public void doAfterThrowing(Throwable throwable) {
// 保存异常日志记录
log.error("发生异常时间 : " + LocalDateTime.now());
log.error("抛出异常 : " + throwable.getMessage());
}
//环绕通知,必须有返回值,否则程序无法继续往下执行,返回空指针异常
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
log.info("权限管理");
//执行目标方法proceed
Object proceed = jp.proceed();
log.info("日志记录");
return proceed;
}
}
3、配置文件
配置文件说明可以查看官网
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="logAspect" class="com.XXX.ssm.aop.LogAspect"/>
<aop:config>
<aop:aspect id="myAspect" ref="logAspect">
<!--切入点-->
<aop:pointcut id="bs" expression="execution(* com.XXX.ssm.service.*.*(..))"/>
<!--前置通知-->
<aop:before method="doBefore" pointcut-ref="bs" />
<!--后置通知,方法正常执行完成会做的事情-->
<aop:after-returning method="doAfterReturning" pointcut-ref="bs" returning="ret"/>
<!--最终通知,总会执行-->
<aop:after method="doAfter" pointcut-ref="bs"/>
<!--环绕通知,方法执行前后都会执行-->
<aop:around method="doAround" pointcut-ref="bs"/>
<!--方法执行过程中发生异常的时候会执行的通知-->
<aop:after-throwing method="doAfterThrowing" pointcut-ref="bs" throwing="throwable"/>
</aop:aspect>
</aop:config>
</beans>
测试代码
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
SysUserService sysUserService = ac.getBean("sysUserService", SysUserService.class);
List<SysUser> list = sysUserService.queryAll();
for (SysUser user:list) {
System.out.println(user);
}
}
四、基于注解的声明式AspectJ
1、AOP常用注解
名称 | 解释 |
---|---|
@Aspect | 注解注释的Class被标识为切面类 |
@Before | 前置通知 方法签名有 JoinPoint 参数 |
@AfterReturning | 后置通知,@AfterReturning注解有 returning 属性,可以在切面方法结束后,返回结果。最好定义为Object |
@Around | 环绕通知,被增强的方法有 ProceedingJoinPoint 参数 |
@AfterThrowing | 异常通知,注解中有 throwing 属性。在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象 |
@After | 最终通知,无论目标方法是否抛出异常,该增强均会被执行。个人理解为try catch finally里的finally一样 |
@Pointcut | 定义切入点 |
2、AOP切面编程控制日志
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
@Aspect // 表明是一个切面类
@Component
public class LogAspect {
Logger log = Logger.getLogger(LogAspect.class);
//进入方法时间戳
private Long startTime;
//方法结束时间戳(计时)
private Long endTime;
/**
* 定义切入点表达式
* 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
* 权限修饰符可以使用默认 第一个*表示返回值类型 ..表示当前包以及其子包下 第二个*表示任意方法 (..)表示任意参数列表
*/
private final String POINTCUT = "execution(* com.woniuxy.ssm.service.*.*(..))";
public LogAspect() {
}
//前置通知,方法之前执行
@Before(POINTCUT)
public void doBefore(JoinPoint joinPoint) {
startTime = System.currentTimeMillis();
log.info("请求开始时间:" + LocalDateTime.now());
log.info("请求参数 : " + Arrays.toString(joinPoint.getArgs()));
}
//后置通知
@After(POINTCUT)
public void doAfter(JoinPoint joinPoint) {
log.info("Logger-->后置通知,方法名:" + joinPoint.getSignature().getName() + ",方法执行完毕");
}
//返回通知 正常结束时进入此方法
@AfterReturning(returning = "ret", pointcut = POINTCUT)
public void doAfterReturning(Object ret) {
endTime = System.currentTimeMillis();
log.info("请求结束时间 : " + LocalDateTime.now());
log.info("请求耗时 : " + (endTime - startTime));
// 处理完请求,返回内容
log.info("请求返回 : " + ret);
}
//异常通知: 1. 在目标方法非正常结束,发生异常或者抛出异常时执行
@AfterThrowing(pointcut = POINTCUT, throwing = "throwable")
public void doAfterThrowing(Throwable throwable) {
// 保存异常日志记录
log.error("发生异常时间 : " + LocalDateTime.now());
log.error("抛出异常 : " + throwable.getMessage());
}
//环绕通知,必须有返回值,否则程序无法继续往下执行,返回空指针异常
@Around(value = POINTCUT)
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
log.info("权限管理");
//执行目标方法proceed
Object proceed = jp.proceed();
log.info("日志记录");
return proceed;
}
}
3、开启AOP自动代理
记得修改spring-config.xml文件,开启自动代理
<!--开启aop自动代理-->
<aop:aspectj-autoproxy/>