如果大家不想看知识点,可以点击目录直接跳转到操作哦,阿里嘎多大家~
一 AOP的含义简介
在开始正式讲解之前,大家一定要明确AOP的具体含义是什么
A : Alone
O : OverWatch
P : Play
AOP的含义呢,大概就是独自(Alone)游玩(Play)守望先锋(OverWatch)这款游戏,众所周知《守望先锋》(Overwatch,简称OW) 是由暴雪娱乐公司开发的一款第一人称射击游戏,于2016年5月24日全球上市,中国大陆地区由网易公司代理......
好了好了,不和大家开玩笑了,AOP的真正含义是Aspect Oriented Programming,意思是面向切面编程,和他对应的是OOP(面向对象程序设计)。大家肯定都对OOP了解的很透彻,毕竟面试热门话题封装继承多态,那么既然已经有了OOP,为什么还要有一个AOP呢?别急,听我慢慢道来~
在以往的开发过程中,如果想要通过OOP去实现代码的重用,那么需要通过组合和继承,同样的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须要修改所有的相关方法。
AOP的思想就是采用横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向的重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
Ps : 重点来啦,面试又可以吹牛逼啦,AOP并不是OOP的替代品,只是OOP的延申和补充。
在AOP中,类与切面的关系如下:
从图上可以看出通过Aspect(切面)分别在Class1和Class2的方法中加入了事务、日志、权限和异常等通用的功能。
二 AOP实现简单接口调用记录日志
准备工作
1 建立相关数据表及实体类等
UserInfo表:
CREATE TABLE `user_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '姓名',
`name` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL COMMENT '电话号码',
`sex` varchar(255) DEFAULT NULL COMMENT '性别',
`idcard` varchar(255) DEFAULT NULL COMMENT '身份证',
`type` varchar(50) DEFAULT NULL COMMENT '1 用户 2 管理员',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='用户表';
LogInfo(日志记录)表:
CREATE TABLE `log_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '姓名',
`msg` varchar(50) DEFAULT NULL COMMENT '信息',
`ip` varchar(50) DEFAULT NULL COMMENT 'ip地址',
`param` varchar(100) DEFAULT NULL COMMENT '参数信息',
`call_time` varchar(50) DEFAULT NULL COMMENT '调用时间',
`type` varchar(50) DEFAULT NULL COMMENT '调用类型',
`method` varchar(50) DEFAULT NULL COMMENT '调用方法',
`url` varchar(60) DEFAULT NULL COMMENT '调用接口URL',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1522478698815574018 DEFAULT CHARSET=utf8 COMMENT='简单日志表';
建好表记得去完善实体类,Dao层,Service和ServiceImpl等等哦(别偷懒!很重要!)
2 引入相关的Jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
开始操作!
1 首先我们需要写一个自定义注解
自定义注解里面你可以自己定义信息比如我写的调用终端,他是一个枚举,具体如何使用看步骤2
2 在你需要的接口上添加此自定义注解
在接口上,可以看到自定义注解中的属性,这是你可以自己填写的具体接口是来自哪个模块,是做什么的,是通过什么方式(如步骤1上的枚举 有manage管理端PC和mobile手机两种)
3 切面操作(划重点!!!很重要!!!)
因为代码太长,各位帅哥靓女可以copy下来看~
@Aspect
@Component
public class WebLogAspect {
@Autowired(required = false)
private LogInfoDao logInfoDao;
/** 换行符 */
private static final String LINE_SEPARATOR = System.lineSeparator();
private final static Logger log = LoggerFactory.getLogger(WebLogAspect.class);
/** 以自定义 @WebLog 注解为切点 */
@Pointcut("@annotation(edu.etime.panda.Aspect.WebLog)")
public void webLog(){
}
/**
* 在切点之前织入
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable{
LogInfo logInfo=new LogInfo();
// 进入方法前 调用开始
System.out.println("切面开始:=============== Strat ===================");
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//获取请求地址URL
String servletPath = request.getServletPath();
//ip地址
String ipAddr = getIpAddr(request);
// 获取 @WebLog 注解上的的描述信息
List<String> listDesc = getAspectLogDescription(joinPoint);
//描述信息
String description=listDesc.get(0);
//调用模块
String businessType=listDesc.get(1);
//调用方式(终端)
String invokingType=listDesc.get(2);
//参数信息
StringBuilder builder=new StringBuilder();
builder.append(description+":"+joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
Object obj= joinPoint.getArgs();
String s = JSON.toJSONString(obj);
//调用时间
SimpleDateFormat simpleDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
logInfo.setCallTime(simpleDate.format(new Date()));
logInfo.setUrl(servletPath);
logInfo.setIp(ipAddr);
logInfo.setMsg(description);
logInfo.setType(businessType);
logInfo.setMethod(invokingType);
logInfo.setParam(s);
logInfoDao.insert(logInfo);
}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
// 接口结束后换行,方便分割查看
System.out.println("切面结束:=========== End ================");
}
/**
* 获取切面注解的描述
* @param joinPoint 切点
* @return 描述信息
* @throws Exception
*/
public List<String> getAspectLogDescription(JoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
StringBuilder description = new StringBuilder("");
StringBuilder businessType=new StringBuilder("");
StringBuilder invokingType=new StringBuilder("");
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description.append(method.getAnnotation(WebLog.class).description());
businessType.append(method.getAnnotation(WebLog.class).businessType());
invokingType.append(method.getAnnotation(WebLog.class).invokingType());
break;
}
}
}
List<String> list=new ArrayList<>();
list.add(description.toString());
list.add(businessType.toString());
list.add(invokingType.toString());
return list;
}
public final static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
}
4 结束!
是不是很快?当然 具体的讲解不可能这么快结束,我们运行这个程序调用一下接口~
我们先不打断点,正常调用一次查询用户的接口
接口调用成功
我们看一下数据库的日志表:
这一条调用的信息已经存入了数据库。
下面我们来debugger一步一步看AOP是如何进行操作的:
在图片的代码里 可以看到我在很多地方打印输出了=======,我在每一个输出的地方都打上断点大家请看!
点击调用后的第一个输出断点
在切点之前织入,进行日志保存在这里操作
第二个断点
进入方法,在调用方法之前
第三个断点
在调用方法结束后,进入。
第四个断点
在切点织入之后
再看数据库
三 总结
其实我呢只是做了一个很简单的AOP记录日志的操作,很多地方实现的并不规范,重点是步骤3中的切面操作,大家可以放在自己的代码上一步一步debug看到是如何拿到参数的,重点就是切面操作,如果文章有疑问或问题,希望大家指出,我会立即修改,祝大家愉快~
敬礼!
salute!