🍃作者:不染心
🍃时间:2021/7/29
一、什么是AOP
- **AOP: 面向切面编程:**将核心业务代码过程比作一个柱体,其他的日志记录,权限校验等就像是横切核心业务的面,这些面需要完成一些非核心的业务。
- OOP: 面向对象编程
AOP能够比OOP更好的分离系统关注点,从而提供模块化的横切关注点。可以把一个复杂的系统看作是由多个关注点来组合实现的。
一个典型的系统可能会包括几个方面的关注点,如业务逻辑,性能,数据存储,日志和调度信息,授权,安全,线程,错误检查等,还有开发过程中的关注点,如易懂,易维护,易追查,易扩展等,这样就完成了多维到多维的映射过程。
二、为什么要AOP
🏷 日常开发问题:
代码混乱:核心业务模块与其他非核心的代码交织在一起,大大影响了代码的模块独立性能,不利于代码的维护,而且分工不明确造成代码混乱。
冗余代码:其实权限的校验,异常的处理,日志的记录可以独立在一个模块给所有的服务公用,写在一起导致代码的分散和冗余。
💥 AOP如何解决:
将日志记录、权限校验等业务代码抽取出来作为一个模块,其只关注切点,针对设点的切点进行编程。
三、与OOP之间的关系
AOP的实现技术有多种,其中与Java无缝对接的是一种称为AspectJ的技术,Spring AOP 与AspectJ 实现原理上并不完全一致,但功能上是相似的。AOP的出现确实解决外围业务代码与核心业务代码分离的问题,但它并不会替代OOP,如果说OOP的出现是把编码问题进行模块化,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。
四、实现AOP
Spring提供两种动态代理机制方式来实现AOP:JDK roxy
、CGLIB
面向切面框架:AspectJ
AOP核心概念:
- 横切关注点:对于哪些方法经行拦截,拦截后怎么处理
- 切面(aspect): 类是对物体特征的抽象,那么切面就是对横切关注点的抽象
- 连接点(joinpoint):被拦截到的方法,实际上拦截点还可以是字段或者是构造器
- 切入点(point cut):对连接点进行拦截的定义
- 通知(advice):拦截到连接点之后要执行的代码,通知通常分为:前置、后置、异常、最终、环绕通知等。
- 目标对象:代理的目标对象
(一)JDK Proxy
📖UserManager.java:
package com.dyl.JDKproxy;
public interface UserManager {
void adduser(String userName, String Userpass);
void delUser(String userName);
}
📖JdkProxy.java:
package com.dyl.JDKproxy;
public class UserManagerImpl implements UserManager{
@Override
public void adduser(String userName, String userPass) {
System.out.println("添加用户名称:" + userName + " 用户密码:" + userPass);
}
@Override
public void delUser(String userName) {
System.out.println("删除用户名称:" + userName);
}
}
📖JdkProxy.java:
package com.dyl.JDKproxy;
public class MyAspect {
public void myBefort(){
System.out.println("方法执行之前");
}
public void myAfter(){
System.out.println("方法执行之后");
}
}
📖JdkProxy.java:
package com.dyl.JDKproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* jdk动态代理实现InvocationHandler接口
* Spring JDK 动态代理需要实现 InvocationHandler 接口,重写 invoke 方法,
* 客户端使用 Java.lang.reflect.Proxy 类产生动态代理类的对象。
*
* 创建 SpringDemo 项目,并在 src 目录下创建 net.biancheng 包。
* 在 net.biancheng 包下创建 UserManager(用户管理接口)、UserManagerImpl(用户管理接口实现类)、
* MyAspect(切面类)和 JdkProxy(动态代理类)。
* 运行 SpringDemo 项目。
*/
public class JdkProxy implements InvocationHandler {
private Object target; //需要代理的对象
final MyAspect myAspect = new MyAspect();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
myAspect.myBefort();
Object result = method.invoke(target, args);
myAspect.myAfter();
return null;
}
// 获取代理对象方法
private Object getJDKProxy(Object targetObject){
// 为目标对象target赋值
this.target = targetObject;
// JDK动态代理只能代理实现了接口的类,从newProxyInstance函数所需的参数就可以看出来
// getInterfaces() 获取这个对象的所有接口
// getClassLoader() 将class文件加载到内存中编程class对象
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
}
public static void main(String[] args) {
JdkProxy jdkProxy = new JdkProxy();
UserManager userManager = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl());
userManager.adduser("dyl", "123456");
userManager.delUser("dyl");
}
}
📖结果:
方法执行之前
添加用户名称:dyl 用户密码:123456
方法执行之后
方法执行之前
删除用户名称:dyl
方法执行之后
Process finished with exit code 0
(二)CDLIB Proxy
📖UserManager.java:
同上
📖UserManagerImpl.java:
同上
📖MyAspect.java:
同上
📖CGLIBProxy.java
:
package com.dyl.CGLIB;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多 AOP 框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类。使用 CGLIB 需要导入 CGLIB 和 ASM 包,即 asm-x.x.jar 和 CGLIB-x.x.x.jar 。如果您已经导入了 Spring 的核心包 spring-core-x.x.x.RELEASE.jar,就不用再导入 asm-x.x.jar 和 cglib-x.x.x.jar 了。
*
* 步骤如下:
* 创建 SpringDemo 项目,并在 src 目录下创建 net.biancheng 包。
* 导入相关 JAR 包。
* 在 net.biancheng 包下创建 UserManager(用户管理接口)、UserManagerImpl(用户管理接口实现类)、MyAspect(切面类)和 CGLIBProxy(动态代理类)。
* 运行 SpringDemo 项目。
*/
public class CGLIBProxy implements MethodInterceptor {
private Object target;
final MyAspect myAspect = new MyAspect();
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
myAspect.myBefort();
Object invoke = method.invoke(target, objects); // 方法执行,参数:target目标对象 arr参数数组
myAspect.myAfter();
return invoke;
}
// 动态获取代理对象方法
public Object getCgliProxy(Object objectTarget){
this.target = objectTarget;
Enhancer enhancer = new Enhancer();
// 设置父类,因为CGLIB是正对指定的类生成一个子类,所以需要指定父类
enhancer.setSuperclass(objectTarget.getClass());
enhancer.setCallback(this); // 设置回调
Object result = enhancer.create(); // 创建并返回代理对象
return result;
}
public static void main(String[] args) {
CGLIBProxy cglibProxy = new CGLIBProxy();
UserManager userManager = (UserManager) cglibProxy.getCgliProxy(new UserManagerImpl()); // 获取代理对象
userManager.adduser("dyl", "123456");
userManager.delUser("dyl");
}
}
(三)AspectJ 框架
- AspectJ是使用面向切面的一个框架
- 它扩展了Java语言(它本身也是一种语言)
- 支持原生Java代码 有自己的编译器
- 将代码翻译成Java字节码文件
- 是为了方便编写AOP代码而出现的
- 使用AOP编程的三个重点 通知 切点 织入
接下来做一个AOP的日志例子,对Man实体操作,对其中指定的方法记录日志
📖 Man.java
package com.dyl.AspectJ_learn;
public class Man {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Man{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void throwException() {
System.out.println("抛出异常");
throw new IllegalArgumentException();
}
}
📖 日志切面类 Logging.java
package com.dyl.AspectJ_learn;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class Logging {
/**
* 定义切入点:该方法无方法体,主要是为了方便同类中其他方法使用此处配置的切入点
* PointCut 的目的就是提供一种方法使得开发者能够选择自己感兴趣的 JoinPoint。
*/
@Pointcut("execution(* com.dyl.AspectJ_learn.Man.*(..))")
private void selectAll() {
}
/**
* 前置通知:在方法执行前执行,如果通知抛出异常,阻止方法运行(应用:各种校验)
* 使用在方法Aspect()上注册的切入点,同时接受JoinPoint切入点对象,可以没有该参数
*/
@Before("selectAll()")
public void beforeAdvice() {
System.out.println("前置通知");
}
/**
* 后置通知
*/
@After("selectAll()")
public void afterAdvice() {
System.out.println("后置通知");
}
/**
* 返回后通知:(应用:常规数据处理)
* 方法正常返回后执行,如果方法中抛出异常,通知无法执行 *
* 必须在方法执行后才执行,所以可以获得方法的返回值
*/
@AfterReturning(pointcut = "selectAll()", returning = "retVal")
public void afterReturningAdvice(Object retVal) {
System.out.println("返回值为:" + retVal.toString());
}
/**
* 环绕通知:(应用:十分强大,可以做任何事情)
*/
@Around("selectAll()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前");
Object result = pjp.proceed();
System.out.println("环绕后");
return result;
}
/**
* 抛出异常通知
*/
@AfterThrowing(pointcut = "selectAll()", throwing = "ex")
public void afterThrowingAdvice(IllegalArgumentException ex) {
System.out.println("这里的异常为:" + ex.toString());
}
}
📖 resources -> Beans_AspectJ.xml
<?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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy />
<bean id="man" class="com.dyl.AspectJ_learn.Man">
<property name="name" value="dyl"/>
<property name="age" value="234"/>
</bean>
<bean id="logging" class="com.dyl.AspectJ_learn.Logging"/>
</beans>
🏷 Main.java
package com.dyl.AspectJ_learn;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans_AspectJ.xml");
Man man = (Man) context.getBean("man");
man.getAge();
man.getName();
// man.throwException();
}
}
📖 结果:
环绕前
前置通知
环绕后
后置通知
返回值为:234
环绕前
前置通知
环绕后
后置通知
返回值为:dyl
💥切点配置说明:
//配置com.bao.User下的add()
execution(* com.bao.User.add(..))
//配置com.bao.User下的所有方法
execution(* com.bao.User.*(..))
//配置所有包下的所有方法
execution(**.*(..))
//配置所有包下的a开头方法
execution(* a*(..))
//配置包下的所有类的所有方法
execution(* com.service.*.*(..)))
五、AOP应用
🔖 日志记录
🔖 权限校验
🔖 返回值操作
🔖 事务
🔖 安全等