写在开头:
我是「猿码天地」,一个热爱技术、热爱编程的IT猿。技术是开源的,知识是共享的!
写博客是对自己学习的总结和记录,如果您对Java、分布式、微服务、中间件、Spring Boot、Spring Cloud等技术感兴趣,可以关注我的动态,我们一起学习,一起成长!
用知识改变命运,让家人过上更好的生活,互联网人一家亲!
关注微信公众号【猿码天地】,获取更多干货技能,一起吃肉喝汤,陪你一起撸代码!
随着互联网技术的深入发展,各个系统的日活用户、访问量、点击量成指数上升,为保证系统的安全性、易用性,每个系统都需要对用户的访问做埋点记录、跟踪,从而获取用户常用的操作习惯,同时也方便系统管理人员对系统做日常记录、跟踪。
AOP:面向切面编程,相对于OOP面向对象编程,Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能继承和实现接口,且类继承只能单继承,阻碍更多行为添加到一组类上,AOP弥补了OOP的不足。还有就是为了清晰的逻辑,让业务逻辑关注业务本身,不用去关心其它的事情,比如事务。
实现方式:Spring的AOP是通过JDK的动态代理和CGLIB实现的。
AOP有一堆术语,主要包括以下:
通知(Advice) 需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用。
连接点(Join point) spring中允许使用通知的地方,基本上每个方法前后抛异常时都可以是连接点。
切点(Poincut) 筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。
切面(Aspect) 通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行。
引入(Introduction) 在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面用到目标类中去。
目标(target) 被通知的对象。也就是需要加入额外代码的对象,真正的业务逻辑被组织织入切面。
织入(Weaving) 把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入。
server:
port: 8081
servlet.context-path: /aop
spring:
aop:
auto: true
AOP切面类
这个是最主要的类,可以使用自定义注解或针对包名实现AOP增强。
1)这里实现了对自定义注解的环绕增强切点,对使用了自定义注解的方法进行AOP切面处理。
2)对方法运行时间进行监控。
3)对方法名,参数名,参数值,对日志描述的优化处理。
在方法上增加@Aspect注解声明切面 使用@Pointcut 注解定义切点,标记方法
使用切点增强的时机注解:
@Before 前置通知, 在方法执行之前执行
@Around 环绕通知, 围绕着方法执行
@AfterReturning 返回通知, 在方法返回结果之后执行
@AfterThrowing 异常通知, 在方法抛出异常之后
@After 后置通知, 在方法执行之后执行
package com.bowen.aspect;
import com.alibaba.fastjson.JSON;
import com.bowen.annotation.OperationLogDetail;
import com.bowen.model.OperationLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* AspectDemo
* AOP切面类
* @author : zhang.bw
* @date : 2020-04-16 14:52
**/
@Aspect
@Component
public class LogAspect {
private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/**
* 定义切点
* 此处的切点是注解的方式,也可以用包名的方式达到相同的效果
* '@Pointcut("execution(* com.bowen.service.impl.*.*(..))")'
*/
//@Pointcut("@annotation(com.bowen.annotation.OperationLogDetail)")
@Pointcut("execution(* com.bowen.controller.*.*(..))")
public void operationLog(){}
/**
* 环绕增强,相当于MethodInterceptor
*/
@Around("operationLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object res = null;
long time = System.currentTimeMillis();
try {
res = joinPoint.proceed();
time = System.currentTimeMillis() - time;
return res;
} finally {
try {
//方法执行完成后增加日志
addOperationLog(joinPoint,res,time);
}catch (Exception e){
LOG.error("LogAspect 操作失败:" + e.getMessage());
}
}
}
/**
* 方法执行完成后增加日志
* @param joinPoint
* @param res
* @param time
*/
private void addOperationLog(JoinPoint joinPoint, Object res, long time){
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
OperationLog operationLog = new OperationLog();
operationLog.setRunTime(time);
operationLog.setReturnValue(JSON.toJSONString(res));
operationLog.setId(UUID.randomUUID().toString());
operationLog.setArgs(JSON.toJSONString(joinPoint.getArgs()));
operationLog.setCreateTime(new Date());
operationLog.setMethod(signature.getDeclaringTypeName() + "." + signature.getName());
operationLog.setUserId("#{currentUserId}");
operationLog.setUserName("#{currentUserName}");
OperationLogDetail annotation = null;
try {
//获取抽象方法
Method method = signature.getMethod();
//获取当前类的对象
Class<?> clazz = joinPoint.getTarget().getClass();
//获取当前类有 OperationLogDetail 注解的方法
method = clazz.getMethod(method.getName(), method.getParameterTypes());
annotation = method.getAnnotation(OperationLogDetail.class);
} catch (Exception e) {
LOG.error("获取当前类有 OperationLogDetail 注解的方法 异常",e);
}
if(annotation != null){
operationLog.setLevel(annotation.level());
operationLog.setDescribe(annotation.detail());
//operationLog.setDescribe(getDetail((signature).getParameterNames(),joinPoint.getArgs(),annotation));
operationLog.setOperationType(annotation.operationType().getValue());
operationLog.setOperationUnit(annotation.operationUnit().getValue());
}
//TODO 这里保存日志
LOG.info("记录日志:" + operationLog.toString());
//operationLogService.insert(operationLog);
}
/**
* 对当前登录用户和占位符处理
* @param argNames 方法参数名称数组
* @param args 方法参数数组
* @param annotation 注解信息
* @return 返回处理后的描述
*/
@Deprecated
private String getDetail(String[] argNames, Object[] args, OperationLogDetail annotation){
Map<Object, Object> map = new HashMap<>(4);
for(int i = 0;i < argNames.length;i++){
map.put(argNames[i],args[i]);
}
String detail = annotation.detail();
try {
detail = "'" + "#{currentUserName}" + "'=》" + annotation.detail();
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object k = entry.getKey();
Object v = entry.getValue();
detail = detail.replace("{{" + k + "}}", JSON.toJSONString(v));
}
}catch (Exception e){
e.printStackTrace();
}
return detail;
}
@Before("operationLog()")
public void doBeforeAdvice(JoinPoint joinPoint){
LOG.info("进入方法前执行.....");
}
/**
* 处理完请求,返回内容
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = "operationLog()")
public void doAfterReturning(Object ret) {
LOG.info("方法的返回值 : " + ret);
}
/**
* 后置异常通知
*/
@AfterThrowing("operationLog()")
public void throwss(JoinPoint jp){
LOG.info("方法异常时执行.....");
}
/**
* 后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
*/
@After("operationLog()")
public void after(JoinPoint jp){
LOG.info("方法最后执行.....");
}
}
package com.bowen.annotation;
import com.bowen.enums.OperationType;
import com.bowen.enums.OperationUnit;
import java.lang.annotation.*;
/**
* AspectDemo
* 自定义注解
* @Target 此注解的作用目标,括号里METHOD的意思说明此注解只能加在方法上面
* @Retention 注解的保留位置,括号里RUNTIME的意思说明注解可以存在于运行时,可以用于反射
* @Documented 说明该注解将包含在javadoc中
* @author : zhang.bw
* @date : 2020-04-16 14:55
**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLogDetail {
/**
* 方法描述:可使用占位符获取参数:{{tel}}
*/
String detail() default "";
/**
* 日志等级:自己定,此处分为1-9
*/
int level() default 0;
/**
* 操作类型(enum):主要是select,insert,update,delete
*/
OperationType operationType() default OperationType.UNKNOWN;
/**
* 被操作的对象(此处使用enum):可以是任何对象,如表名(user),或者是工具(redis)
*/
OperationUnit operationUnit() default OperationUnit.UNKNOWN;
}
package com.bowen.enums;
/**
* AspectDemo
* 操作类型
* @author : zhang.bw
* @date : 2020-04-16 14:58
**/
public enum OperationType {
/**
* 操作类型
*/
UNKNOWN("unknown"),
DELETE("delete"),
SELECT("select"),
UPDATE("update"),
INSERT("insert");
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
OperationType(String s) {
this.value = s;
}
}
package com.bowen.enums;
/**
* AspectDemo
* 被操作的单元
* @author : zhang.bw
* @date : 2020-04-16 15:00
**/
public enum OperationUnit {
/**
* 被操作的单元
*/
UNKNOWN("unknown"),
USER("user"),
EMPLOYEE("employee"),
Redis("redis");
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
OperationUnit(String value) {
this.value = value;
}
}
package com.bowen.model;
import lombok.Data;
import java.util.Date;
/**
* AspectDemo
* 日志记录对象
* @author : zhang.bw
* @date : 2020-04-16 15:01
**/
@Data
public class OperationLog {
private String id;
private Date createTime;
/**
* 日志等级
*/
private Integer level;
/**
* 被操作的对象
*/
private String operationUnit;
/**
* 方法名
*/
private String method;
/**
* 参数
*/
private String args;
/**
* 操作人id
*/
private String userId;
/**
* 操作人
*/
private String userName;
/**
* 日志描述
*/
private String describe;
/**
* 操作类型
*/
private String operationType;
/**
* 方法运行时间
*/
private Long runTime;
/**
* 方法返回值
*/
private String returnValue;
@Override
public String toString() {
return "OperationLog{" +
"id='" + id + '\'' +
", createTime=" + createTime +
", level=" + level +
", operationUnit='" + operationUnit + '\'' +
", method='" + method + '\'' +
", args='" + args + '\'' +
", userId='" + userId + '\'' +
", userName='" + userName + '\'' +
", describe='" + describe + '\'' +
", operationType='" + operationType + '\'' +
", runTime=" + runTime +
", returnValue='" + returnValue + '\'' +
'}';
}
}
package com.bowen;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootAopApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAopApplication.class, args);
}
}
package com.bowen.controller;
import com.bowen.annotation.OperationLogDetail;
import com.bowen.enums.OperationType;
import com.bowen.enums.OperationUnit;
import com.bowen.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* AspectDemo
* 测试接口请求
* @author : zhang.bw
* @date : 2020-04-16 14:03
**/
@Controller
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/**
* 访问路径 http://localhost:8081/user/findUserNameByTel?tel=1234567
* @param tel 手机号
* @return userName
*/
@ResponseBody
@RequestMapping("/findUserNameByTel")
@OperationLogDetail(detail = "通过手机号获取用户名",level = 3,operationUnit = OperationUnit.USER,operationType = OperationType.SELECT)
public String findUserNameByTel(@RequestParam("tel") String tel){
return userService.findUserName(tel);
}
}
package com.bowen.service;
/**
* AspectDemo
* service
* @author : zhang.bw
* @date : 2020-04-16 15:05
**/
public interface UserService {
/**
* 获取用户信息
* @return
* @param tel
*/
String findUserName(String tel);
}
package com.bowen.service.impl;
import com.bowen.aspect.LogAspect;
import com.bowen.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* AspectDemo
* service实现
* @author : zhang.bw
* @date : 2020-04-16 15:08
**/
@Service
public class UserServiceImpl implements UserService {
private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);
@Override
public String findUserName(String tel) {
LOG.info("tel:" + tel);
return "zhangsan";
}
}
<!-- spring-boot aop依赖配置引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
进入方法前执行.....
tel:1234567
记录日志:OperationLog{id='5aea4821-206b-408f-9e7f-d1145af786fb', createTime=Fri Apr 17 11:40:12 CST 2020, level=3, operationUnit='user', method='com.bowen.controller.UserController.findUserNameByTel', args='["1234567"]', userId='#{currentUserId}', userName='#{currentUserName}', describe='通过手机号获取用户名', operationType='select', runTime=11, returnValue='"zhangsan"'}
方法最后执行.....
方法的返回值 : zhangsan
该demo没有将日志写入数据库,如有需要,可在LogAspect.java文件的//TODO 这里保存日志 位置实现改功能。
写在结尾:
关注【源码天地】,做一个潮流的技术人!