提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
AOP:面向切面编程
OOP:面向对象编程
面向切面编程:基于OOP思想之上新的编程思想
指在程序运行期间,将某段代码动态的切入到指定的方法的指定位置上进行运行这种编程方式,面向切面编程
场景:计算器
动态代理
接口
public interface Calculator {
public int add(int i,int j);
public int sub(int i,int j);
public int mul(int i,int j);
public int div(int i,int j);
}
被代理的对象
public class MyMathCalculator implements Calculator {
@Override
public int add(int i, int j) {
int result = i+j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i-j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i/j;
return result;
}
@Override
public int div(int i, int j) {
int result = i*j;
return result;
}
}
创建代理对象
public class CalculatorProxy {
//JDK动态代理创建target的代理对象
public static Calculator getProxy(Calculator target) {
Calculator proxy = (Calculator)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result=null;
String name = method.getName();
System.out.println(name+"执行了");
//利用反射执行目标方法
try {
result = method.invoke(target, args);
//返回值必须返回出去才能拿到真正执行后的返回值
System.out.println(name+"方法执行结果是"+result);
}catch (Exception e){
System.out.println(name+"方法执行出现异常"+e.getMessage());
}
return result;
}
});
return proxy;
}
}
测试
public class AOPTest {
//被代理的对象
Calculator target = new MyMathCalculator();
@Test
public void test() {
Calculator proxy = CalculatorProxy.getProxy(target);
Class<?>[] interfaces = proxy.getClass().getInterfaces();
for (Class<?> anInterface : interfaces) {
System.out.println("代理对象实现的接口:"+anInterface);
}
Class<?>[] interfaces1 = target.getClass().getInterfaces();
for (Class<?> aClass : interfaces1) {
System.out.println("目标对象实现的接口"+aClass);
}
proxy.add(1,2);
}
}
代理对象实现的接口:interface inter.Calculator
目标对象实现的接口interface inter.Calculator
add执行了
add方法执行结果是3
JDK的动态代理有一个缺点,就是被代理的对象必须实现了接口,否则无法使用JDK动态代理
创建的代理对象实际上也实现了接口
SpringAOP
由以上我们就能看出JDK动态代理的缺点,就是实现麻烦,而且必须实现接口,Spring AOP的原理就是动态代理,解决了上述这些缺点
AOP的几个专业术语:
- 连接点(join Point):程序执行的某个特定位置,比如(类开始初始化前,初始化后,方法前,方法后,异常后) 一个类或者一段程序代码拥有一些具有边界的特定点,这些特定点称为连接点
- 切点(pointcut):每个程序都拥有多个连接点,比如一个类有2个方法,这2个方法就是连接点,连接点就是程序中具体的事物,AOP 通过切点来定位特定的连接点,连接点相当于数据库中的记录,而切点相当于是查询记录的条件。在程序中,切点是连接点位置的集合
- 增强(advice):就是我们要具体做的事情,也就在原有的方法之上添加新的能力
目标对象:就是我们要增强的类;
引入:特殊的增强,为类添加属性和方法。哪怕这个类没有任何的属性和方法我们也可以通过aop去添加方法和属性实现逻辑 - 织入(weaving):就是把增强添加到目标类具体的连接点上的过程
代理:一个类被AOP织入增强后产生的结果类,他是原类和增强后的代理类,更具代理s不同的方式,代理类可能是和原类具有相同接口的类,也可能是原类的子类 - 切面(Aspect):切面由切点和增强组成,他既包含横切的定义,也包括了连接点的定义,spring aop就是负责实施切面的框架,他将切面定义为横切逻辑织入到切面所指定的连接点
简单实现
Spring的配置类
@Configuration
@ComponentScan("sop")
@EnableAspectJAutoProxy
public class MainConfig {
}
接口
public interface Calculator {
public int add(int i, int j);
public int sub(int i, int j);
public int mul(int i, int j);
public int div(int i, int j);
}
被代理的对象
@Component
public class MyMathCalculator implements Calculator {
@Override
public int add(int i, int j) {
int result = i+j;
System.out.println(result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i-j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i/j;
return result;
}
@Override
public int div(int i, int j) {
int result = i*j;
return result;
}
}
切面类
@Component
@Aspect
public class LogUtil {
@Pointcut("execution(public int sop.inter.MyMathCalculator.*(..))")
public void pointcut(){};
/*
告诉Spring每个方法什么时候运行
*/
//前置通知
@Before("pointcut()")
public static void logStart() {
System.out.println("start");
}
//返回通知
@AfterReturning("pointcut()")
public static void logReturn() {
System.out.println("return");
}
//结束通知
@After("pointcut()")
public static void logEnd() {
System.out.println("end");
}
//异常通知
@AfterThrowing("pointcut()")
public static void logException() {
System.out.println("exception");
}
}
测试
public class AOPTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext(MainConfig.class);
//不能在new了。一定要从容器中拿
//Calculator target = new MyMathCalculator();
//1、从容器中拿对象,注意,如果想要用类型,一定要用它的接口类型,不要用本类
Calculator bean = ioc.getBean(Calculator.class);
System.out.println(bean);
System.out.println(bean.getClass()); //class com.sun.proxy.$Proxy17
// 说明aop的底层就是动态代理,容器中是代理对象,不是本类对象,所以要用接口获取bean
bean.add(1, 2);
}
}
sop.inter.MyMathCalculator@24aed80c
class com.sun.proxy.$Proxy17
start
3
end
return
细节1:容器中创建的是代理对象,而不是MyMathCalculator 对象,因此获取bean的时候,如果使用类型的话,只能用被代理类实现的接口
细节2:因为本例中MyMathCalculator 实现了接口,因此Spring会使用JDK创建动态代理对象
@Component
public class MyMathCalculator {
public int add(int i, int j) {
int result = i+j;
System.out.println(result);
return result;
}
public int sub(int i, int j) {
int result = i-j;
return result;
}
public int mul(int i, int j) {
int result = i*j;
return result;
}
public int div(int i, int j) {
int result = i/j;
return result;
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext(MainConfig.class);
//不能在new了。一定要从容器中拿
//Calculator target = new MyMathCalculator();
//1、从容器中拿对象,注意,如果想要用类型,一定要用它的接口类型,不要用本类
MyMathCalculator bean = ioc.getBean(MyMathCalculator.class);
System.out.println(bean);
System.out.println(bean.getClass()); //class com.sun.proxy.$Proxy17
// 说明aop的底层就是动态代理,容器中是代理对象,不是本类对象,所以要用接口获取bean
bean.add(1, 2);
}
sop.inter.MyMathCalculator@291caca8
class sop.inter.MyMathCalculator$$EnhancerBySpringCGLIB$$f0bd2383
start
3
end
return
细节3:如果这个MyMathCalculator 没有实现接口,Spring会采用CGLIB的方式创建代理对象,那么获取bean的方式如果是通过接口的话就获取不到了,只能用被代理的类
细节4: 通知的执行顺序
try {
@Before()
fun()
@AfterReturning
}catch (){
@AfterThrowing
finally {
@After()
}
正常执行:@Before()----> @After()-----> @AfterReturning
异常执行:@Before()----> @After()-----> @AfterThrowing
细节5:在通知方法运行的时候,拿到目标方法的详细信息——JoinPoint
@Before("pointcut()")
/*
JoinPoint joinPoint:封装了当前目标方法的详细信息
*/
public static void logStart(JoinPoint joinPoint) {
//获取到目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取方法签名
Signature signature = joinPoint.getSignature();
//获取方法名
String name = signature.getName();
System.out.println("start");
}
细节6:returning 和throwing 来指定要获取返回值和异常
//返回通知
@AfterReturning(value = "pointcut()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result) {
System.out.println("return"+result);
}
//异常通知
@AfterThrowing(value = "pointcut()" ,throwing ="excption" )
public static void logException(JoinPoint joinPoint, Exception excption) {
System.out.println("exception");
}
Exception 指定了Spring接受的异常,如果本例中写的是NullPointerException,那么该通知方法只能接受NullPointerException
start
end
Exception in thread "main" java.lang.ArithmeticException: / by zero
exception
//异常通知
@AfterThrowing(value = "pointcut()" ,throwing ="excption" )
public static void logException(JoinPoint joinPoint, NullPointerException excption) {
System.out.println("exception");
}
start
end
对比:exception没打印
result同Exception
细节7:通知方法时Spring利用反射调用的,所以上面参数不能乱写
细节8:环绕通知,利用反射执行目标方法
/*
四合一就是环绕通知
ProceedingJoinPoint pjp
*/
@Around( "pointcut()")
public Object myaroud(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object proceed=null;
//利用反射调用目标方法
try {
System.out.println("环绕前置通知"); //代替@Before
proceed = pjp.proceed(args);//推进目标方法运行的
System.out.println("环绕返回通知"); //代替 @AfterReturning
}catch (Exception e){
System.out.println("环绕异常通知");//代替 @AfterThrowing
throw new RuntimeException(e); //一定抛出去,否则异常通知捕获不到异常
}finally {
System.out.println("环绕后置通知");//代替 @After
}
return proceed;//这个返回值,就是目标方法的返回值
}
环绕通知,就是利用反射执行目标方法,ProceedingJoinPoint pjp必不可少,它是JoinPoint的子类,如果目标方法有返回值,环绕通知必须return一个返回值,次返回值就是目标方法的返回值,可以利用环绕通知取代其他四个通知(如上述代码)
有了环绕通知后,执行顺序就成了
【普通前置】
环绕前置
try{
环绕执行,目标方法执行
环绕返回
}catch (){
环绕出现异常
}finally{
环绕处理
}
目标方法执行
【普通后置】
【普通方法返回/普通方法异常】
环绕前置—》普通前置—》目标方法执行—》环绕正常返回/异常—》环绕后置—》普通返回/异常
为了让普通异常通知拿到异常,环绕通知的异常必须要抛出异常
环绕通知可以通过操作直接影响目标方法的中,其他的不会影响目标方法的执行
多切面运行顺序
场景1:新增一个切面ValidateAspect
并对LogUtil 做简单的修改,去掉环绕通知,测试两个切面的通知默认执行顺序
@Component
@Aspect
public class ValidateAspect {
@Before("sop.inter.LogUtil.pointcut()")
public static void logStart(JoinPoint joinPoint) {
System.out.println("ValidateAspect 前置通知");
}
@AfterReturning(value = "sop.inter.LogUtil.pointcut()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result) {
System.out.println("ValidateAspect返回通知");
}
@After( "sop.inter.LogUtil.pointcut()")
public static void logEnd() {
System.out.println("ValidateAspect后置通知");
}
@AfterThrowing(value = "sop.inter.LogUtil.pointcut()" ,throwing ="excption" )
public static void logException(JoinPoint joinPoint, NullPointerException excption) {
System.out.println("ValidateAspect异常通知");
}
}
@Component
@Aspect
public class LogUtil {
@Pointcut("execution(public int sop.inter.MyMathCalculator.*(..))")
public void pointcut(){};
/*
告诉Spring每个方法什么时候运行
*/
//前置通知
@Before("pointcut()")
/*
JoinPoint joinPoint:封装了当前目标方法的详细信息
*/
public static void logStart(JoinPoint joinPoint) {
//获取到目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取方法签名
Signature signature = joinPoint.getSignature();
//获取方法名
String name = signature.getName();
System.out.println("LogUtil 前置通知");
}
//返回通知
@AfterReturning(value = "pointcut()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result) {
System.out.println("LogUtil 返回通知");
}
//结束通知
@After( "pointcut()")
public static void logEnd() {
System.out.println("LogUtil 后置通知");
}
//异常通知
@AfterThrowing(value = "pointcut()" ,throwing ="excption" )
public static void logException(JoinPoint joinPoint, NullPointerException excption) {
System.out.println("LogUtil 异常通知");
}
}
LogUtil 前置通知
ValidateAspect 前置通知
被代理类add
ValidateAspect后置通知
ValidateAspect返回通知
LogUtil 后置通知
LogUtil 返回通知
由测试结果可以看出切面LogUtil 嵌套ValidateAspect 执行,为什么LogUtil 先执行呢?因为默认按照首字母从小到大,越小优先级越高
场景2:@Order指定顺序,值越小优先级越高
@Component
@Aspect
@Order(1)
public class ValidateAspect {
@Component
@Aspect
@Order(2)
public class LogUtil {
ValidateAspect 前置通知
LogUtil 前置通知
被代理类add
LogUtil 后置通知
LogUtil 返回通知
ValidateAspect后置通知
ValidateAspect返回通知
场景3:考虑环绕通知
@Component
@Aspect
@Order(2)
public class LogUtil {
@Pointcut("execution(public int sop.inter.MyMathCalculator.*(..))")
public void pointcut(){};
/*
告诉Spring每个方法什么时候运行
*/
//前置通知
@Before("pointcut()")
/*
JoinPoint joinPoint:封装了当前目标方法的详细信息
*/
public static void logStart(JoinPoint joinPoint) {
//获取到目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取方法签名
Signature signature = joinPoint.getSignature();
//获取方法名
String name = signature.getName();
System.out.println("LogUtil 前置通知");
}
//返回通知
@AfterReturning(value = "pointcut()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result) {
System.out.println("LogUtil 返回通知");
}
//结束通知
@After( "pointcut()")
public static void logEnd() {
System.out.println("LogUtil 后置通知");
}
//异常通知
@AfterThrowing(value = "pointcut()" ,throwing ="excption" )
public static void logException(JoinPoint joinPoint, NullPointerException excption) {
System.out.println("LogUtil 异常通知");
}
/*
四合一就是环绕通知
ProceedingJoinPoint pjp
*/
@Around( "pointcut()")
public Object myaroud(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object proceed=null;
//利用反射调用目标方法
try {
System.out.println("LogUtil环绕前置通知"); //代替@Before
proceed = pjp.proceed(args);//推进目标方法运行的
System.out.println("LogUtil环绕返回通知"); //代替 @AfterReturning
}catch (Exception e){
System.out.println("LogUtil环绕异常通知");//代替 @AfterThrowing
throw new RuntimeException(e); //一定抛出去,否则异常通知捕获不到异常
}finally {
System.out.println("LogUtil环绕后置通知");//代替 @After
}
return proceed;//这个返回值,就是目标方法的返回值
}
}
ValidateAspect 前置通知
LogUtil环绕前置通知
LogUtil 前置通知
被代理类add
LogUtil环绕返回通知
LogUtil环绕后置通知
LogUtil 后置通知
LogUtil 返回通知
ValidateAspect后置通知
ValidateAspect返回通知
环绕只在当前切面内作用
AOP使用场景
1、AOP加日志保存到数据库
2、AOP做权限验证
3、AOP做安全检查
4、AOP做事务控制