spring AOP 原理和介绍
github 的spring中文文档 :IOC 介绍:https://github.com/DocsHome/spring-docs/blob/master/pages/core/overview.md
介绍
面向切面编程(Aspect-oriented Programming 简称AOPAOP) ,是相对面向对象编程(Object-oriented Programming 简称OOP)的框架,作为OOP的一种功能补充. OOP主要的模块单元是类(class)。而AOP则是切面(aspect)。切面会将诸如事务管理这样跨越多个类型和对象的关注点模块化。例如处理日志、安全管理、事务管理等
AOP提供了非常强大的功能,用来对IOC做补充。通俗点说的话就是在程序运行期间。在不修改原有代码的情况下 增强跟主要业务没有关系的公共功能代码到 之前写好的方法中的指定位置 这种编程的方式叫AOP。
AOP的底层用的代理,代理是一种设计模式
静态代理
弊端:需要为每一个被代理的类创建一个“代理类”,虽然这种方式可以实现,但是成本太高
动态代理(AOP的底层是用的动态)
jdk动态代理 :必须保证被代理的类实现了接口,
cglib动态代理 :不需要接口,
https://blog.csdn.net/u013126379/article/details/52121096
静态代理:代理类和被代理类实现相同的接口,代理类中调用被代理类对象的方法,并且在被代理的方法前后添加需要处理的代码。
jdk动态代理实现
@Test
public void test02(){
ICaculator proxy = (ICaculator)createProxy(new Caculator());
System.out.println(proxy.getClass());
proxy.add(1,1);
Player proxy1 = (Player)createProxy(new IPlayer("徐庶"));
System.out.println(proxy1.getClass());
proxy1.start();
proxy1.play();
}
public Object createProxy(Object needProxy){
/*动态创建代理类,传参类的加载器,接口,和委托执行的处理类*/
ClassLoader classLoader = needProxy.getClass().getClassLoader();
Class<?>[] interfaces = needProxy.getClass().getInterfaces();
// MyInoveHandler myInoveHandler = new MyInoveHandler(needProxy);
InvocationHandler myInoveHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (args == null || args.length == 0) {
System.out.println("无参");
} else {
System.out.println("输入参数为" + Arrays.asList(args));
}
Object invoke = method.invoke(needProxy, args);
System.out.println("返回结果:" + invoke);
return invoke;
}
};
Object o = Proxy.newProxyInstance(classLoader, interfaces, myInoveHandler);
return o;
}
public class MyInoveHandler implements InvocationHandler {
// 传入被代理的对象
private Object tatget;
public MyInoveHandler(Object tatget) {
this.tatget = tatget;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(args == null || args.length == 0){
System.out.println("无参");
}else {
System.out.println("输入参数为"+ Arrays.asList(args));
}
/*
* 执行被代理的方法,传被代理的对象和参数
* */
Object invoke = method.invoke(tatget, args);
System.out.println("返回结果:" + invoke);
return invoke;
}
}
public interface ICaculator {
/*加减乘除*/
Integer add(Integer i,Integer j);
Integer sub(Integer i,Integer j);
Integer mul(Integer i,Integer j);
Integer dev(Integer i,Integer j);
}
cglib实现动态代理
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
@Test
public void test4(){
Player o = (Player)cglibProxy(new IPlayer("赵云"));
System.out.println(o.getClass());
o.start();
o.play();
}
public Object cglibProxy(Object object){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(object.getClass());
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("打印参数");
Object invoke = method.invoke(object, args);
return invoke;
}
});
Object o = enhancer.create();
return o;
}
AOP 概念
让我们从定义一些核心AOP概念和术语开始。 这些术语不是特定于Spring的。 不幸的是,AOP术语不是特别直观。 但是,如果Spring使用自己的术语,那将更加令人困惑。
- 切面(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。 在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以
@Aspect
注解(@AspectJ 注解方式)来实现。 - 连接点(Join point): 在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
- 通知(Advice): 在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”, “before” and “after”等等。通知的类型将在后面的章节进行讨论。 许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
- 切点(Pointcut): 匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。
- 引入(Introduction): 声明额外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现
IsModified
接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。 - 目标对象(Target object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。
- AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
- 织入(Weaving): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。 Spring和其他纯Java AOP框架一样,是在运行时完成织入的。
Spring AOP包含以下类型的通知:
- 前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。
- 后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。
- 后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。
- 后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。
- 环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型,。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。
AOP的应用场景
- 日志管理
- 权限认证
- 安全检查
- 事务控制
Aop使用
1、引入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<!--aspectjweaver已经被spring-aspects引入,所以引入一个依赖即可-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
2、添加aspectj注解识别的配置
<?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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.blog"/>
<!--开启aop注解的识别功能-->
<aop:aspectj-autoproxy/>
</beans>
// java注解方式开启aop识别
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
3、配置切面类
@Aspect
@Component
public class LogUtil {
//通过切点表达式定义目标对象
@Before("execution(* com.blog.service..*.*(..))")
public void before(){
System.out.println("前置通知");
}
@After("execution(* com.blog.service..*.*(..))")
public void after(){
System.out.println("后置通知");
}
@AfterReturning("execution(* com.blog.service..*.*(..))")
public void afterReturn(){
System.out.println("后置返回通知");
}
@AfterThrowing("execution(* com.blog.service..*.*(..))")
public void afterThrowing(){
System.out.println("后置异常通知");
}
}
// 目标对象实例,当目标对象方法执行时,通过注解配置的通知也将执行。
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserDao userDao;
public User get(Integer id) {
System.out.println("查询User");
return userDao.get(id);
}
public void add(User user) {
System.out.println("添加User");
userDao.add(user);
}
public void delete(Integer id) {
System.out.println("删除User");
userDao.delete(id);
}
public void update(User user) {
System.out.println("修改User");
userDao.update(user);
}
}
通过容器获取切面类时会自动获取到代理类对象
@Test
public void test5(){
/*aop在类有接口的情况下,默认是用sdk动态代理,代理类相当于和目标类实现相同的接口
* 此时需要通过接口的全限定类名称获取代理类,或者根据id属性获取
* 输出为class com.sun.proxy.$Proxy21
* */
// UserService userServiceImpl = context.getBean(UserService.class);
/* UserService userServiceImpl = (UserService) context.getBean("UserServiceImpl");
System.out.println(userServiceImpl.getClass());*/
/*
* 如果没有实现接口,通过cglib实现,cglib是通过以目标类为父类来实现动态代理的,
* 所以通过目标类来接收动态代理对象
* 输出为class com.blog.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$e64a2ccf
* */
// UserServiceImpl userServiceImpl = (UserServiceImpl) context.getBean("UserServiceImpl");
UserServiceImpl userServiceImpl = context.getBean("UserServiceImpl",UserServiceImpl.class);
System.out.println(userServiceImpl.getClass());
}
切点表达式
Spring AOP支持使用以下AspectJ切点标识符(PCD),用于切点表达式:
- execution: 用于匹配方法执行连接点。 这是使用Spring AOP时使用的主要切点标识符。 可以匹配到方法级别 ,细粒度
- within: 只能匹配类这级,只能指定类, 类下面的某个具体的方法无法指定, 粗粒
- this: 匹配实现了某个接口: this(com.xyz.service.AccountService)
- target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实
例。 - args: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中变量是给定类型的实例。 AOP) where the arguments are instances of the given types.
- @target: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注
- @args: 限制匹配连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
- @within: 限制与具有给定注解的类型中的连接点匹配(使用Spring AOP时在具有给定注解的类型中声明的方法的执行)。
- @annotation:限制匹配连接点(在Spring AOP中执行的方法具有给定的注解)。
示例
@Aspect
@Component
public class LogUtil {
// 标识任意返回值,com.blog.service任何包任何类,任何方法,任何参数
@Before("execution(* com.blog.service..*.*(..))")
public void before(){
System.out.println("前置通知");
}
// 标识com.blog.service任何包任何类
@After("within(com.blog.service..*)")
public void after(){
System.out.println("后置通知");
}
// 匹配带有Logger注解的方法,有注解才会执行,注解需要运行时注解,既编译成class文件后还存在
@AfterReturning("@annotation(jdk.nashorn.internal.runtime.logging.Logger)")
public void afterReturn(){
System.out.println("后置返回通知");
}
@AfterThrowing("execution(* com.blog.service..*.*(..))")
public void afterThrowing(){
System.out.println("后置异常通知");
}
/*
匹配代理实现了UserService 接口的任何实现类
this(com.blog.service.UserService)
配置参数为指定类型的任意实现类
args(java.io.Serializable)
* */
}
&&,
||
和 !
等符号进行表达式合并操作
// 标识com.blog.service任何包任何类并且带有Logget注解
@After("within(com.blog.service..*) && @annotation(jdk.nashorn.internal.runtime.logging.Logger)")
通知的顺序
正常: @Before --> @After --> @AfterReturning
异常: @Before --> @After --> @AfterThrowing
通知方法中获取目标对象信息
JoinPoint
接口提供很多有用的方法::
getArgs()
: 返回方法参数.getThis()
: 返回代理对象.getTarget()
: 返回目标对象.getSignature()
:返回正在通知的方法的描述.toString()
: 打印方法被通知的有用描述.
// 标识任意返回值,com.blog.service任何包任何类,任何方法,任何参数
@Before("execution(* com.blog.service..*.*(..))")
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
System.out.println("方法名是"+name);
Object[] args = joinPoint.getArgs();
System.out.println("参数是" + Arrays.asList(args));
System.out.println("前置通知");
}
通知方法中获取目标对象执行结果
@AfterReturning(value = "@annotation(jdk.nashorn.internal.runtime.logging.Logger)",
returning = "result")
public void afterReturn(Object result){
System.out.println("返回值为:" + result);
System.out.println("后置返回通知");
}
通知方法中获取目标对象抛出的异常
public void afterThrowing(Exception ex){
// 输出异常的栈信息
StringWriter stringWriter = new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter,true));
System.out.println("后置异常通知"+ stringWriter.getBuffer().toString());
}
其他
spring对通知方法要求并不高,可以随意的定义方法的访问修饰符、返回值,等信息,但是不会影响返回结果
表达式的抽取,复用
@Aspect
@Component
public class LogUtil {
// 抽取共同的表达式,在同一个地方定义一次即可
@Pointcut("execution(* com.blog.service..*.*(..))")
public void pointCut(){
};
// 使用定义的切点表达式
@Before("pointCut()")
public void before(JoinPoint joinPoint){
System.out.println("前置通知");
}
@AfterReturning(value = "pointCut()",
returning = "result")
public void afterReturn(Object result){
System.out.println("返回值为:" + result);
System.out.println("后置返回通知");
}
获取注解设置的属性值
// 获取注解设置的属性值
/*
* @annotation不用写成"@annotation(jdk.nashorn.internal.runtime.logging.Logger)"
* 会自动根据value值找到参数中对应入参的类型
* 可以通过获取注解属性值输出特定信息
* */
@After("within(com.blog.service..*) && @annotation(logger)")
public void after(Logger logger){
String name = logger.name();
System.out.println(name);
System.out.println("后置通知");
}
@Around环绕通知
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint){
//获取参数,和方法名
joinPoint.getArgs();
joinPoint.getSignature().getName();
Object proceed = null;
try {
System.out.println("环绕前置通知");
proceed = joinPoint.proceed();
System.out.println("环绕后置通知");
} catch (Throwable throwable) {
System.out.println("环绕异常通知");
throwable.printStackTrace();
}finally {
System.out.println("环绕返回通知");
}
return proceed;
}
正常情况下的执行顺序
环绕前置通知
前置通知
目标方法
环绕后置通知
环绕返回通知
后置通知
后置返回通知
xml配置方式实现aop
定义对应的切面类
public class LogAspectjForXml {
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
System.out.println("方法名是"+name);
Object[] args = joinPoint.getArgs();
System.out.println("参数是" + Arrays.asList(args));
System.out.println("前置通知");
}
@After("within(com.blog.service..*) && @annotation(logger)")
public void after(Logger logger){
String name = logger.name();
System.out.println(name);
System.out.println("后置通知");
}
public void afterReturn(Object result){
System.out.println("返回值为:" + result);
System.out.println("后置返回通知");
}
public void afterThrowing(Exception ex){
// 输出异常的栈信息
StringWriter stringWriter = new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter,true));
System.out.println("后置异常通知"+ stringWriter.getBuffer().toString());
}
public Object around(ProceedingJoinPoint joinPoint){
//获取参数,和方法名
joinPoint.getArgs();
joinPoint.getSignature().getName();
Object proceed = null;
try {
System.out.println("环绕前置通知");
proceed = joinPoint.proceed();
System.out.println("环绕后置通知");
} catch (Throwable throwable) {
System.out.println("环绕异常通知");
throwable.printStackTrace();
}finally {
System.out.println("环绕返回通知");
}
return proceed;
}
}
<?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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.blog"/>
<!-- 切面类注册为bean -->
<bean class="com.blog.aspectj.LogAspectjForXml" id="aspectj"/>
<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="aspectj">
<aop:pointcut id="pointCut" expression="execution(* com.blog.service..*.*(..))"/>
<aop:before method="before" pointcut-ref="pointCut"/>
<!-- <aop:after method="after" pointcut="within(com.blog.service..*) && @annotation(logger)"/>-->
<aop:after method="after" pointcut="within(com.blog.service..*) and @annotation(logger)"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointCut" throwing="ex"/>
<aop:around method="around" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
</aop:config>
</beans>