整理
https://blog.csdn.net/xlgen157387/article/details/82497594
https://www.cnblogs.com/yeya/p/11169014.html
正文
AOP可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。所谓”切面”,就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性
官方文档地址<
代理模式
什么是代理模式?
代理模式,也叫委托模式,其定义是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
我们可以用举一个电影演员拍戏的例子
一般来说,演员最主要的工作就是演戏,其他的事可以交给他的经纪人去做,例如谈合同,安排档期等等,
而负责这些场外工作的经纪人就相当于Proxy (代理主题角色。负责读具体主题角色的引用,通过真实角色的业务逻辑方法来实现抽象方法,并在前后可以附加自己的操作。)
负责核心业务的演员就是 RealSubject 。(具体主题角色,也就是被代理的对象,是业务逻辑的具体执行者。)
Subject:抽象主题角色。可以是抽象类也可以是接口,是一个最普通的业务类型定义。
代理模式分为静态代理和动态代理,静态代理是我们自己创建一个代理类,而动态代理是程序自动帮我们生成一个代理类,可以在程序运行时再生成对象
AOP
主要概念(术语)
- 切面(aspect):通常是一个类,里面可以定义切入点和通知
- 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。
- 连接点(joinpoint):
类的哪些方法可以增强,这些方法称为链接点
被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截(增强)到的方法,实际上连接点还可以是字段或者构造器。
- 切入点(pointcut):对连接点进行拦截的定义
实际被真正增强的方法
- 通知(advice):
实际增强的逻辑部分
所谓通知指的就是指拦截到连接点之后要执行的代码,通知以下五类。
- 前置通知:在我们执行目标方法之前运行(@Before)
- 后置通知:在我们目标方法运行结束之后 ,不管有没有异常(@After)
- 异常通知:在我们的目标方法出现异常后运行(@AfterThrowing)
- 环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知(@Around)
- 返回通知:在我们的目标方法正常返回值后运行(@AfterReturning)
- 目标对象:代理的目标对象
- 织入(weave):将切面应用到目标对象并导致代理对象创建的过程
- 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
实现原理
AOP代理:AOP框架创建的对象,代理就是目标对象的加强。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
cglib 和 jdk 动态代理
Spring 提供了两种方式来生成代理对象: JDKProxy 和 Cglib,具体使用哪种方式生成由AopProxyFactory 根据 AdvisedSupport 对象的配置来决定。
默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。
Spring中的AOP代理还是离不开Spring的IOC容器,代理的生成,管理及其依赖关系都是由IOC容器负责
现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些。
1. JDK 动态接口代理
一个符合某一接口的实例,生成目标类的代理对象。
JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。
在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;
JDK 动态代理主要涉及到 java.lang.reflect 包中的两个核心类:Proxy 和 InvocationHandler。
- 调用newProxyInstance方法
参数1:类加载器 2:增强的方法需要实现的多个接口
3:InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。
Proxy 利用 InvocationHandler 动态创建
JDK动态代理类实现了InvocationHandler接口,重写的invoke方法。
JDK动态代理的基础是反射机制(method.invoke(对象,参数))Proxy.newProxyInstance()
代码示例
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args)
proxy是最终生成的代理对象;
method 是被代理目标实例的某个具体方法;
args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
public class TimeHandler implements InvocationHandler{
private Object target;
public TimeHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("吃饭之前要洗手");
//调用真正的方法
Object retVal = method.invoke(target, args);
System.out.println("吃饭之后要洗碗");
return retVal;
}
}
2. CGLib
CGLib 全称为 Code Generation Library,是一个强大的高性能,高质量的代码生成类库
可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
CGLib创建的动态代理对象性能比JDK创建的动态代理对象的性能高不少,但是CGLib在创建代理对象时所花费的时间却比JDK多得多
实现方式
Spring AOP 借助了 ApectJ的语法风格
1.经典的基于代理的AOP
2.@AspectJ注解驱动的切面
3.纯POJO切面
4.注入式AspectJ切面
使用
注解
- Spring支持AspectJ的注解式切面编程。
@Aspect 声明一个切面(类上)
@PointCut 声明切点
/**
* 定义切点 所有类上加了Controller注解的
*/
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *) || within(@org.springframework" +
".stereotype.Controller *)")
public void beanAnnotatedWithController() {
}
/**
* 定义切点 所有方法上加了OperationLog注解的
*/
@Pointcut(value = "@annotation(operationLog)", argNames = "operationLog")
private void operationLogMethod(OperationLog operationLog) {
}
使用@After、@Before、@Around定义建言(advice),可直接将拦截规则(切点)作为参数。
- @After 在方法执行之后执行(方法上)
- @Before 在方法执行之前执行(方法上)
- @Around 在方法执行之前与之后执行(方法上)
/**
* 环绕通知
*
* @param joinPoint 切点
* @param operationLog 注解
*/
@Around(value = "beanAnnotatedWithController() && operationLogMethod(operationLog)", argNames = "joinPoint,operationLog")
public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String path = request.getRequestURI();
}
```java /**// 解析Spring EL表达式,获取到参数值
Object[] objects = joinPoint.getArgs();
EvaluationContext context = new StandardEvaluationContext();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] names = methodSignature.getParameterNames();
for (int i = 0; i < names.length; i++) {
context.setVariable(names[i], objects[i]);
}
- 异常通知 *
- @param joinPoint 切点
- @param operationLog 自定义注解
*/
@AfterThrowing(value = “beanAnnotatedWithController() && operationLogMethod(operationLog)”, throwing = “ex”,
public void afterThrowing(JoinPoint joinPoint, OperationLog operationLog, Exception ex) {argNames = "joinPoint,operationLog,ex")
2. 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上)
<a name="TIJC9"></a>
## 应用场景
如权限认证、日志、事务。<br />AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来
<a name="mskZX"></a>
### 1. 切面实现防止重复提交
1. 自定义注解 `@NoRepeatSubmit` 标记所有Controller中的提交请求
1. 通过AOP 对所有标记了 `@NoRepeatSubmit` 的方法拦截
1. 在业务方法执行前,获取当前用户的 token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)
1. 业务方法执行后,释放锁
```java
@Aspect
@Component
public class RepeatSubmitAspect {
private final static Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(noRepeatSubmit)")
public void pointCut(NoRepeatSubmit noRepeatSubmit) {
}
@Around("pointCut(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
int lockSeconds = noRepeatSubmit.lockTime();
HttpServletRequest request = RequestUtils.getRequest();
Assert.notNull(request, "request can not null");
// 此处可以用token或者JSessionId
String token = request.getHeader("Authorization");
String path = request.getServletPath();
String key = getKey(token, path);
String clientId = getClientId();
boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
if (isSuccess) {
LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功, 执行进程
Object result;
try {
result = pjp.proceed();
} finally {
// 解锁
redisLock.releaseLock(key, clientId);
LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
LOGGER.info("tryLock fail, key = [{}]", key);
return new ResultBean(ResultBean.FAIL, "重复请求,请稍后再试", null);
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
2. 使用AOP处理请求或记录日志
面向切面的编程,或AOP,是一种编程技术,允许程序模块化横向切割关注点,或横切典型的责任划分,如日志和事务管理。
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.具体切面代码
/**
* 日志写入管理
* zjy
*/
@Aspect
@Component
public class ControllerAspect {
private final static Logger logger = LoggerFactory.getLogger(ControllerAspect.class);
@Autowired
@Qualifier("logServiceImpl")
private LogService logService;
@Autowired
private UserService userService;
@Pointcut("execution(* com.honghe.managerTool.controller.CommandController.*(..)) "
+ "|| execution(* com.honghe.managerTool.controller.UserController.*(..)) ")
public void aspect() {//拦截所有类下所有方法
}
//配置前置通知,使用在方法aspect()上注册的切入点
@Before("aspect()")
public void before(JoinPoint joinPoint) {
String[] paramNames = ((CodeSignature) joinPoint
.getSignature()).getParameterNames();
Object[] paramValues = joinPoint.getArgs();
String userIp = "127.0.0.1";
String oprationName = joinPoint.getSignature().getName();
switch (oprationName){
case "start":
oprationName=Operation.START.value;
break;
case "stop":
oprationName=Operation.STOP.value;
break;
case "restart":
oprationName=Operation.RESTART.value;
break;
default:
return;
}
if(paramValues.length>0){
if(paramValues[0] instanceof HttpServletRequest){
HttpServletRequest request = (HttpServletRequest)paramValues[0];
userIp = ParamUtil.getIpAddress(request);
}
}
Log log = new Log(oprationName,new Date(),userIp,1);
logService.saveLog(log);
}
//配置后置通知,使用在方法aspect()上注册的切入点
@After("aspect()")
public void after(JoinPoint joinPoint){
logger.info("after out successfully");
}
}
对http请求解析
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* @author: zhaojianyu
* @create: 2018-10-11 09:11
**/
@Aspect
@Component
public class HttpAspect {
private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);
/**
* 拦截所有类下所有方法
*/
@Pointcut("execution(* com.zjy.blog.blog_start.controller.HelloContraller.*(..)) ")
public void aspect() {
}
/**
* 配置前置通知,使用在方法aspect()上注册的切入点
*/
@Before("aspect()")
public void before(JoinPoint joinPoint) {
logger.info("方法开始执行"+new Date());
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//url
logger.info("url={}",request.getRequestURL());
logger.info("uri={}",request.getRequestURI());
//请求方法
logger.info("method={}",request.getMethod());
//请求ip地址
logger.info("ip={}",request.getRemoteAddr());
//类名 类方法
logger.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName()+"="+joinPoint.getSignature().getName());
//参数
logger.info("args={}",joinPoint.getArgs());
}
@After("aspect()")
public void after() {
logger.info("方法执行完毕1"+new Date());
}
@AfterReturning(returning = "object",pointcut = "aspect()")
public void afterReturning(Object object) {
logger.info("方法执行完毕2"+new Date() + object);
}
}
参考文章:
https://blog.csdn.net/JinXYan/article/details/89302126