面向切面编程AOP
SpringBoot整合aop日志管理
1.前期准备
1.1 配置
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
1.2 涉及知识点
- Java基础自定义注解、反射
- Spring AOP
- SpringBoot
2.aop(Aspect Oriented Programming)
可以对程序进行扩展,而不需要修改源代码。
使用代理完成面向切面的功能。(注意代理不是Spring的代理,只是使用代理。)
日志、登录权限、事务处理等等功能都可以使用AOP解决
2.1 aop的五种通知
- 前置通知(Before):在目标方法或者说连接点被调用前执行的通知;
- 后置通知(After):指在某个连接点完成后执行的通知;
- 返回通知(After-returning):指在某个连接点成功执行之后执行的通知;
- 异常通知(After-throwing):指在方法抛出异常后执行的通知;
- 环绕通知(Around):指包围一个连接点通知,在被通知的方法调用之前和之后执行自定义的方法。
2.2 使用注解
//在AOP类上添加注解
@Aspect
@Component
//添加切入点
/*
切入表达式execution(* com.mf.core..*.*(..))
切入点是一个方法
第一个*表示切入点的返回类型,如果为*,代表所有返回类和无返回值
第一个..表示该包下所有的子包。如果写一个.那么表示只在根包下。
第二个*表示类,可以写类名、如果写*表示所有类。
第三个*表示方法名,如果写*表示所有方法
第二个..表示方法的参数类型,如果写..表示所有参数类型,可以写参数类型名
*/
@Pointcut("execution(* com.woniu..*.*(..))")
//添加通知
@Before
@AfterReturning
@After
@AfterThrowing
@Around
@AfterReturning和@AfterThrowing互斥
2.3 实现aop日志功能
实现效果:用户在浏览器登录web页面,对应的操作会被记录到数据库中。
实现思路:自定义一个注解,将注解加到登录操作上,使用aop环绕通知代理带有注解的方法,在环绕前进行日志准备,执行完方法后进行日志入库。
自定义注解类
package com.woniu.core.annotation;
import java.lang.annotation.*;
/**
* 自定义注解类
*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface SystemControllerLog {
String descrption() default "";
}
切面处理类
- 拦截带有注解的方法
- 使用Log日志类将日志录入数据库
package com.woniu.core.aop;
import com.woniu.core.annotation.SystemControllerLog;
import com.woniu.core.common.HttpContextUtil;
import com.woniu.dao.LogDao;
import com.woniu.pojo.Log;
import com.woniu.pojo.User;
import com.woniu.service.LogService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Date;
@Aspect
@Component
public class SystemLogAspect {
@Resource
private LogService logService;
@Resource
private LogDao logDao;
/***
* 定义controller切入点拦截规则,拦截SystemControllerLog注解的方法
*/
@Pointcut("@annotation(com.woniu.core.annotation.SystemControllerLog)")
public void controllerAspect(){}
/***
* 拦截控制层的操作日志
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("controllerAspect()")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取方法
Method method = methodSignature.getMethod();
//获取方法的注解
SystemControllerLog systemControllerLog
=method.getAnnotation(SystemControllerLog.class);
String operateType = systemControllerLog.descrption();
Log systemLog = new Log();
systemLog.setOp(operateType);
//获取session中的用户
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Subject subject = SecurityUtils.getSubject();
User loginUser = (User) subject.getSession().getAttribute("loginUser");
systemLog.setUid(loginUser.getUid());
systemLog.setUname(loginUser.getUname());
//获取ip地址
String ip = HttpContextUtil.getIpAddress();
systemLog.setIp(ip);
Object result = null;
try {
result=joinPoint.proceed();
systemLog.setOp("正常");
} catch (SQLException e) {
// 相当于异常通知部分
systemLog.setOp("失败");// 设置操作结果
} finally {
// 相当于最终通知
systemLog.setLtime(new Date());//获取时间
logDao.insertSelective(systemLog);// 添加日志记录
}
return result ;
}
}
将注解加到需要记录的方法体上
- 这个登录后跳转页面的controller层的方法,也可以在其他方法体上加注解。
@SystemControllerLog(descrption = "用户登录")
@RequestMapping("main")
public String main() {
return "main";
}
获取用户ip
- 首先我们需要获取HttpServletRequest
- Controller层通过RequestContextHolder.getRequestAttributes()获取HttpServletRequest
- 非controller层可能会出现空指针问题
package com.woniu.core.common;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class HttpContextUtil {
/**
* 为了获取 HttpServletRequest
* @return
*/
public static HttpServletRequest getRequest(){
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
/**
* 获取ip地址
* @return
*/
public static String getIpAddress(){
HttpServletRequest request=getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip==null|| ip.length()==0||"unknown".equalsIgnoreCase(ip)){
ip=request.getHeader("Proxy-Client-IP");
}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
结语
获取用户ip注意的点:
- X-Forwarded-For 这是一个 Squid 开发的字段,只有在通过了 HTTP 代理或者负载均衡服务器时才会添加该项。格式为X-Forwarded-For: client1, proxy1, proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip。现在大部分的代理都会加上这个请求头。
- Proxy-Client-IP/WL- Proxy-Client-IP 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL- Proxy-Client-IP是他的weblogic插件加上的头。
- HTTP_CLIENT_IP 有些代理服务器会加上此请求头。
- X-Real-IP nginx代理一般会加上此请求头。
- 获取客户端ip,直接使用ip = request.getRemoteAddr (),虽然获取到的可能是代理的ip不是客户端的ip,但这个获取到的ip基本上是不可能伪造的。