动态代理背景

动态代理里面有三个对象:原始对象,代理对象,目标对象
这三个对象可以理解成找工作的人(原始对象),帮你找工作的中介(代理对象),找工作这件事(目标对象)
现在以一个例子来理解:
就是你需要实现一个计算器,计算器的功能需要又计算的操作,还需要有日志的记录,比如执行前记录现在执行的是那一个方法,计算的两个数是什么,执行后记录方法名和计算的结果
现在的难点就是在这个日志的功能你需要怎么实现,可能你想到最简单粗暴的方法就是直接输出,比如

package math;

public class MathImpl implements MathI
{
@Override
public int add(int x, int y)
{
System.out.println("日志:方法名add,参数"+x+"和"+y);
int result=x+y;
System.out.println("日志:方法名add,结果"+result);
return result;
}
}

那么这样写就是有问题的,
你将日志文件的代码写在事务代码里面,这样做成代码混乱,
还有就是代码的重复利用不高,就比如在计算器里面加一个除法的功能,那么就得也在除法的方法里面增加日志的代码,而且这两个日志的代码完全是相似相同的

现在我们就想创建一个动态代理的类,这个类就是自动地帮我们写日志文件
动态代理类是可以为任何的目标对象创建代理对象,动态代理的方式有两种:

  1. 基于接口实现的动态代理:JDK代理,要求代理的对象必须有实现的接口,
  2. 基于继承实现动态代理:Cglib动态代理
    下面代码演示JDK自动代理
package math;

public interface MathI
{
//这里只实现计算器的加的方法
int add(int x,int y);
}
package math;

public class MathImpl implements MathI
{
@Override
public int add(int x, int y)
{
// System.out.println("日志:方法名add,参数"+x+"和"+y);
int result=x+y;
// System.out.println("日志:方法名add,结果"+result);
return result;
}
}
package math;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class ProxyUtil
{
private MathImpl mathimpl;

public ProxyUtil(MathImpl mathimpl)
{
this.mathimpl = mathimpl;
}

public Object getProxy()
{
/*
* 这里使用的是JDK动态代理,这里需要三个参数
* 类加载器:因为这个动态代理对象是这件事需要做的时候
* 而创建的一个动态代理对象来帮你做这件事,
* 那么动态代理对象的创建是需要类的,
* 类的加载需要加载器的
* 接口:这个是代理对象想帮你去做事情,
* 但是它得知道你需要做什么事情,所以代理对象
* 就是通过你实现的接口来知道你需要做什么事情的
* 因为你实现的方法就是在接口里面的
* 执行处理器:就是动态代理对象知道需要做什么了,
* 这个执行处理器就是具体的做法
* 这个执行处理器是一个接口InvocationHandler,
* 你可以实现这个接口,这个参数就直接写this
* 或者直接创建一个内部类重写抽象方法
*/
ClassLoader loader=this.getClass().getClassLoader();
Class[] interfaces=mathimpl.getClass().getInterfaces();

return Proxy.newProxyInstance(loader, interfaces, new InvocationHandler()
{
/*
* 这个方法有三个参数
* proxy,
* method 这个是动态代理对象需要执行的方法
* args 这个是动态代理对象需要执行的方法的参数
* 代理对象帮你做事情,但是要保证结果的一致性
* 那么执行方法的时候就是调用了目标对象,
* 那么就是使用目标对象的方法,结果肯定是一致性的
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
Logger.before(method.getName(), Arrays.toString(args));
Object result = method.invoke(mathimpl, args);
Logger.after(method.getName(), result);
return result;
}
});
}
}
package math;

public class Logger
{
public static void before(String methodName,String args)
{
System.out.println("方法:"+methodName+",参数:"+args);
}
public static void after(String methodName,Object result)
{
System.out.println("方法:"+methodName+",结果:"+result);
}
}
package math;

public class Test
{
public static void main(String[] args)
{
// MathI math=new MathImpl();
// int add = math.add(2, 3);
// System.out.println(add);
ProxyUtil proxy = new ProxyUtil(new MathImpl());
MathI proxy2 = (MathI)proxy.getProxy();
int add=proxy2.add(1, 3);
System.out.println(add);
}
}
结果
方法:add,参数:[1, 3]
方法:add,结果:4
4

AOP概述

  1. AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充。
    面向对象是纵向继承机制,比如上面的背景中,如果需要写日志功能使用的是面向对象的方法来写,就是可能是写一个MathI的子类,继承了MathI的所有方法,然后在代码里面增加日志的代码,结果还是代码混乱,代码重复率不高等问题
    面向切面编程是横向抽取机制,比如上面的背景中的动态代理的写法,将日志功能的代码抽取出来写在一个日志的类里面,然后通过一些方法让他作用在MathI类里面,这样就不像面向对象的方法一样纵向增加代码,而是横向增加代码,而且这样可以将业务代码和非业务代码进行分离
  2. AOP编程操作的主要对象是切面(aspect),
    切面就是上面背景的Logger的类,
    而切面用于模块化横切关注点(公共功能),
    横切关注点就是上面背景的日志功能的方法,
    模块化就是将非业务代码全部放在一块。
  3. 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。
    这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
  4. AOP的好处:
    每个事物逻辑位于一个位置,代码不分散,便于维护和升级
    业务模块更简洁,只包含核心业务代码
    AOP和代理对象是一样的
  5. spring框架:AOP动态代理(以注释的方式配置切面)_spring

AOP术语

横切关注点

从每个方法中抽取出来的同一类非核心业务的代码,比如上面背景的日志方法。

切面(Aspect)

封装横切关注点信息的类,每个关注点体现为一个通知方法。比如上面背景的Logger类。

通知(Advice)

切面必须要完成的各个具体工作,和横切关注点是同一个东西,放在切面里面就叫通知,在业务代码就叫横切关注点

目标(Target)

被通知的对象,比如上面的MathI类

代理(Proxy)

向目标对象应用通知之后创建的代理对象

连接点(Joinpoint)

横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。

例如:类某个方法调用前、调用后、方法捕获到异常后等。比如上面的背景中的日志信息,执行前有一个日志信息,那么抽取出来的日志信息的代码的位置就是一个连接点

在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:

横坐标:各个模块中的可执行方法

纵坐标:每个方法中的各个关注点

spring框架:AOP动态代理(以注释的方式配置切面)_连接点_02


切入点(pointcut)

切入点其实就是一个判断表达式,通知作用于连接点的条件,定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。图解

spring框架:AOP动态代理(以注释的方式配置切面)_动态代理_03

AspectJ

概述
AspectJ:Java社区里最完整最流行的AOP框架。
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
在Spring中启用AspectJ注解支持

  1. 导入JAR包
    com.springsource.net.sf.cglib-2.2.0.jar
    com.springsource.org.aopalliance-1.0.0.jar
    com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
    spring-aop-4.0.0.RELEASE.jar
    spring-aspects-4.0.0.RELEASE.jar
  2. 引入aop的命名空间
  3. 配置
    配置文件写上< aop:aspectj-autoproxy>
    当Spring IOC容器侦测到bean配置文件中的< aop:aspectj-autoproxy>元素时,会自动为与AspectJ切面匹配的bean创建代理

用AspectJ注解声明切面

  1. 要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。
  2. 当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
  3. 在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
  4. 通知是标注有某种注解的简单的Java方法。
  5. AspectJ支持5种类型的通知注解:
    ① @Before:前置通知,在方法执行之前执行
    ② @After:后置通知,在方法执行之后执行
    ③ @AfterRunning:返回通知,在方法返回结果之后执行
    ④ @AfterThrowing:异常通知,在方法抛出异常之后执行
    ⑥ @Around:环绕通知,围绕着方法执行

AOP使用

切入点表达式
作用:通过表达式的方式定位一个或多个具体的连接点。
切入点表达式的语法格式:
​​​execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))​​​ ,
在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
具体看例子

当前连接点

切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。

spring框架:AOP动态代理(以注释的方式配置切面)_spring_04

通知
概述

  1. 通知是在具体的连接点上要执行的操作。
  2. 一个切面可以包括一个或者多个通知。
  3. 通知所使用的注解的值往往是切入点表达式。

分类

  1. 前置通知:在方法执行之前执行的通知,使用@Before注解
  2. 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候,使用@After注解
  3. 返回通知:方法执行完执行的通知,方法有异常就不执行
  4. 异常通知:只在连接点抛出异常时才执行异常通知
    1)将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
    2)如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
  5. 环绕通知:所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
    1)对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
    2)在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
    3)注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。

重用切入点定义

  1. 在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
  2. 在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
  3. 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
  4. 其他通知可以通过方法名称引入该切入点

指定切面的优先级

  1. 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
  2. 切面的优先级可以通过实现Ordered接口或利用@Order注解指定。
  3. 实现Ordered接口,getOrder()方法的返回值越小,优先级越高。
  4. 若使用@Order注解,序号出现在注解中

上面的AOP使用的具体,都看下面的例子,这样好理解

package aop;

public interface MathI
{
//这里只实现计算器的加的方法
int add(int x,int y);
}
package aop;

import org.springframework.stereotype.Component;

@Component
public class MathImpl implements MathI
{
@Override
public int add(int x, int y)
{
// System.out.println("日志:方法名add,参数"+x+"和"+y);
int result=x/y;
// System.out.println("日志:方法名add,结果"+result);
return result;
}
}
<?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 http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

<context:component-scan base-package="aop"/>
<!-- 这里说明里面有动态代理的注解的类都自动创建动态代理对象 -->
<aop:aspectj-autoproxy/>
</beans>
package aop;

import java.util.Arrays;

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.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect //标注当前类是切面类
public class MyloggerAspect
{
/*
* @Pointcut(value="execution( * aop.*.*(..))")
* 这个是定义一个重用的的切入点,下面的切入点表达式想使用这个切入点的话,可以这样写
* @Before(value = "jane()"),这样写的效果和
* @Before(value = "execution( * aop.*.*(..))")的效果是一样的
*/
@Pointcut(value="execution( * aop.*.*(..))")
public void jane() {}

/*
* @Before(value = "")
* 是将方法设置为前置通知,方法执行前进行调用
* 而且注解必须设置value参数,value是切入点表达式
* 使用execution()这个方法进行解析切入点表达式
* 注意:
* @Before(value = "execution(public int aop.MathImpl.add(int, int))")
* 这样写这个方法就只能作用于add的方法,但是我们需要作用的是还有加减乘除的方法
* 我们还可以像写过滤器一样就行占位符的写法
* @Before(value = "execution( * aop.*.*(..))")
* 前面的一个*是代表任意的修饰符和返回值
* 第二个*代表的是任意的接口
* 第三个是这个类的任意方法
* 后面..代表的是任意参数数量和类型
* 例如:
* @Before(value = "execution(public * aop.*.*(..))")
* 这里表示aop包下所有的接口的所有公共方法
*
* @Before(value = "execution(public int aop.*.*(..))")
* 这里表示aop包下所有的公共接口中返回值是int的方法
*
* @Before(value = "execution(public int aop.*.*(int,..))")
* 这里表示aop包下所有的公共接口中返回值是int并且第一个参数是int的方法
*
* execution (* *.add(int,..)) || execution(* *.sub(int,..))
* 任意类中第一个参数为int类型的add方法或sub方法
*
* !execution (* *.add(int,..))
* 匹配不是任意类中第一个参数为int类型的add方法
*
* @Order(1)是设置切面的优先级
* 如果有多个切面作用于同一个连接点,那么切面的优先级是可以设置的
* 就是使用@Order(1)进行设置,里面的值越小优先级越高,默认值是int的最大值
* 如果你是写负数的的话就是没有效果的
*/
// @Before(value = "execution(public int aop.MathImpl.add(int, int))")
// @Before(value = "execution( * aop.*.*(..))")
@Before(value = "jane()")
@Order(1)
public void beforeMethod(JoinPoint jionpoint)
{
Object[] args = jionpoint.getArgs();//获取参数
String name = jionpoint.getSignature().getName();//获取方法名
System.out.println("前置通知:名称"+name+"参数"+Arrays.toString(args));
}

/*
* @After(value = "execution( * aop.*.*(..))")
* 后置通知,作用于方法执行后,这里的后是指finally语句块里面
* 所以这里的后置通知无论方法有没有异常都会执行
*/
@After(value = "execution( * aop.*.*(..))")
public void after()
{
//这里一般用来关闭资源
System.out.println("后置通知");
}

/*
* @AfterReturning(value = "execution( * aop.*.*(..))",returning = "result")
* 返回通知:作用于方法执行后,这里是指方法执行后的语句块,如果有异常的话就不会执行这个方法了
* 如果想得到方法返回的结果,那么就需要配置两个地方
* 1. 在注解里面写上returning设置接收方法返回值的变量名
* 2. 在方法里面写上一个和上面的变量名相同名字的形参
*/
@AfterReturning(value = "execution( * aop.*.*(..))",returning = "result")
public void afterreturning(JoinPoint joinpoint,Object result)
{
String name = joinpoint.getSignature().getName();
System.out.println("返回通知:名称:"+name+"方法"+result);
}

/*
* @AfterThrowing:将这个方法标注为异常通知(也叫例外通知)
* 异常通知:作用于方法执行时如果有异常就执行这个方法,如果没有就不执行,和try-catch语句一样的
* 如果想得到异常信息也得配置两个信息:
* 1. 在注解里面写上throwing设置接收方法返回值的变量名
* 2. 在方法里面写上一个和上面的变量名相同名字的形参,而变量的类型自己定义
* 注意形参的类型:
* 如果是NullPointerException,那么方法执行的时候有空指针异常的时候才执行这个方法
* 如果是其他的异常就不会执行这个方法的,因为spring赋值的时候会找形参对应类型的方法进行赋值的
* 所以根据这个可以根据异常类型处理特定的异常
*/
@AfterThrowing(value = "execution( * aop.*.*(..))",throwing = "ex")
public void afterthrowing(NullPointerException ex)
{
System.out.println("异常信息:"+ex);
}

/*
* @Around
* 环绕通知,和直接写动态代理类是一样的
*/
@Around(value = "execution( * aop.*.*(..))")
public Object around(ProceedingJoinPoint joinpoint)
{
Object result=null;
try
{
System.out.println("环绕前置");
//执行这个方法
result = joinpoint.proceed();
System.out.println("环绕返回");
} catch (Throwable e)
{
System.out.println("环绕异常");
e.printStackTrace();
}finally
{
System.out.println("环绕后置");
}
return result;
}
}
package aop;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test
{
public static void main(String[] args)
{
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("aop.xml");
MathI mathI = ac.getBean("mathImpl", MathI.class);
System.out.println(mathI.add(1, 3));
}
}

以XML方式配置切面

除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
配置
在bean配置文件中,所有的Spring AOP配置都必须定义在< aop:config>元素内部。对于每个切面而言,都要创建一个< aop:aspect>元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供< aop:aspect>元素引用。
声明切入点

  1. 切入点使用< aop:pointcut>元素声明。
  2. 切入点必须定义在< aop:aspect>元素下,或者直接定义在< aop:config>元素下。
    ① 定义在< aop:aspect>元素下:只对当前切面有效
    ② 定义在< aop:config>元素下:对所有切面都有效
  3. 基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。

声明通知

  1. 在aop名称空间中,每种通知类型都对应一个特定的XML元素。
  2. 通知元素需要使用来引用切入点,或用直接嵌入切入点表达式。
  3. method属性指定切面类中通知方法的名称
<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

<context:component-scan base-package="aopxml"></context:component-scan>
<aop:config>
<aop:aspect ref="mylogger">
<aop:pointcut expression="execution(* aopxml.MathI.*(..))" id="cut"/>
<aop:before method="before" pointcut-ref="cut"/>
<!-- 或者写成这样,都是一样的,上面是切入点引用 -->
<!-- <aop:before method="before" pointcut="execution(* aopxml.MathI.*(..))"/> -->

</aop:aspect>
</aop:config>
</beans>