背景
因公司一后台管理系统现可能由非技术人员进行操作,为防止错误操作或出问题时找不到背锅人的情况,现计划在原有框架下加入日志切面。
代码
表
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT '' COMMENT '用户名',
`operation` varchar(50) DEFAULT '' COMMENT '用户操作',
`method` varchar(200) DEFAULT '' COMMENT '请求方法',
`params` varchar(2000) DEFAULT '' COMMENT '请求参数',
`exception` varchar(1000) DEFAULT '' COMMENT '异常信息',
`ip` varchar(64) DEFAULT '' COMMENT 'IP地址',
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_cd_op` (`create_date`,`operation`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8 COMMENT='系统后台操作日志'
实体
public class SysLog implements Serializable {
private Long id;
private String username;
private String operation;
private String method;
private String params;
private String exception;
private String ip;
private Date createDate;
注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
LogOperationEnum value();
}
枚举
public enum LogOperationEnum {
UPDATE_ORDER_INFO("更新订单信息"),
ADD_SYS_CTRL("新增系统配置"),
UPDATE_SYS_CTRL("更新系统配置"),
UPDATE_SYS_LOAN_NUM("更新资方账期"),
ADD_SIGH_CONF("新增签约配置"),
UPDATE_SIGH_CONF("更新签约配置"),
ADD_COPYWRITING_CONF("新增文案配置"),
UPDATE_COPYWRITING_CONF("更新文案配置"),
ADD_COMPANY_INFO("新增公司信息"),
UPDATE_COMPANY_INFO("更新公司信息"),
;
public final String desc;
LogOperationEnum(String desc) {
this.desc = desc;
}
public String getDesc(){
return desc;
}
}
切面
@Aspect
@Component
public class LogAspect {
public static final int PARAMS_MAX_LENGTH = 2000;
public static final int EXCEPTION_MAX_LENGTH = 1000;
@Resource
private ISysLogService sysLogService;
@Pointcut("@annotation(com.q.pay.order.admin.annotation.Log)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result;
SysLog sysLog = new SysLog();
//操作名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log LogAnnotation = method.getAnnotation(Log.class);
sysLog.setOperation(LogAnnotation.value().desc);
//方法名
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName);
//参数
Object[] args = joinPoint.getArgs();
if (ObjectUtil.isNotEmpty(args[0])) {
String params = JsonUtil.toJSONString(args[0]);
int paramsLength = params.length();
int maxLength = paramsLength > PARAMS_MAX_LENGTH ? PARAMS_MAX_LENGTH : paramsLength;
sysLog.setParams(params.substring(0, maxLength));
}
//操作人
if (ObjectUtil.isNotEmpty(args[1])) {
sysLog.setUsername(args[1].toString());
}
//IP地址
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
sysLog.setIp(IPUtils.getIpAddr(request));
try {
result = joinPoint.proceed();
} catch (Throwable e) {
//异常信息
String exception = e.toString();
int exceptionLength = exception.length();
int maxLength = exceptionLength > EXCEPTION_MAX_LENGTH ? EXCEPTION_MAX_LENGTH : exceptionLength;
sysLog.setException(exception.substring(0, maxLength));
//异步插入操作日志
sysLogService.asyncInsertSelective(sysLog);
throw e;
}
//异步插入操作日志
sysLogService.asyncInsertSelective(sysLog);
return result;
}
}
IPUtils
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
/**
* 获取IP地址
*
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
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.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
接着在对应Controller中加入响应的@Log注解即可。
但由于原有Controller框架原有导致日志拦截失效。
切面失效原因
首先有一个抽象基类BaseController
public abstract class BaseController<PO, VO> {
/**
* 查询列表
*/
@RequestMapping("/query")
@ResponseBody
public Object list(@Validated VO vo,
@RequestParam(value = "pageIndex", required = false) Integer pageIndex,
@RequestParam(value = "pageSize", required = false) Integer pageSize, BindingResult bindingResult) throws Exception {
BindingResultUtil.judgeParam(bindingResult);
PO config = buildQuery(vo);
Page page = query(config, pageIndex, pageSize);
return ResponseUtil.buildSuccess(page);
}
/**
* 更新
*/
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
public Object updateInfo(String loginUserName, @Validated(value = {Update.class}) VO vo, BindingResult bindingResult) throws Exception {
BindingResultUtil.judgeParam(bindingResult);
PO po = buildQuery(vo);
return update(po, loginUserName) > 0 ? ResponseUtil.buildSuccess() : ResponseUtil.buildError();
}
/**
* 新增
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
public Object addInfo(String loginUserName, @Validated(value = {Save.class}) VO vo, BindingResult bindingResult) throws Exception {
BindingResultUtil.judgeParam(bindingResult);
PO po = buildQuery(vo);
return add(po, loginUserName) > 0 ? ResponseUtil.buildSuccess() : ResponseUtil.buildError();
}
protected abstract PO buildQuery(VO vo);
protected abstract Page query(PO po, Integer pageIndex, Integer pageSize);
public abstract Integer update(PO po, String loginUserName) throws Exception;
public abstract Integer add(PO po, String loginUserName) throws Exception;
}
而子类Controller大致如下:
@Controller
@RequestMapping(path = "/companyInfo")
public class CompanyInfoController extends BaseController<CompanyInfo, CompanyInfoVO> {
@Resource
private ICompanyInfoService companyInfoService;
@Log(LogOperationEnum.ADD_COMPANY_INFO)
@Override
public Integer add(CompanyInfo po, String loginUserName) {
companyInfoService.insert(po);
return 1;
}
@Log(LogOperationEnum.UPDATE_COMPANY_INFO)
@Override
public Integer update(CompanyInfo po, String loginUserName) {
companyInfoService.update(po);
return 1;
}
@Override
protected Page query(CompanyInfo po, Integer pageIndex, Integer pageSize) {
return companyInfoService.query(po, pageIndex, pageSize);
}
@Override
protected CompanyInfo buildQuery(CompanyInfoVO vo) {
return new CompanyInfo()
.setId(vo.getId())
.setCompanyId(vo.getCompanyId())
.setCompanyName(vo.getCompanyName())
.setAdvanceRepayment(vo.getAdvanceRepayment())
.setCompanyStatus(vo.getCompanyStatus())
.setCreateDate(vo.getCreateDate())
.setUpdateDate(vo.getUpdateDate())
.setExt1(vo.getExt1())
.setExt2(vo.getExt2())
.setExt3(vo.getExt3());
}
}
一开始不知道拦截为啥失效,做了非常多尝试,本来已经打算把注解打在Service层了,后来发现是方法自调用导致的失效。
因为子类加了日志注解并重写的update和add方法是在BaseController中的updateInfo和addInfo中调用,这样事实上就出现了同一个类中方法的自我调用,这跟Spring事务、缓存、异步注解是同样道理,AOP默认同一个类中方法的自我调用是不会生效的,因为此时调用的是原目标对象方法而不是代理对象方法。
这是由于 Spring AOP (包括动态代理和 CGLIB 的 AOP) 的限制导致的. Spring AOP 并不是扩展了一个类(目标对象), 而是使用了一个代理对象来包装目标对象, 并拦截目标对象的方法调用. 这样的实现带来的影响是: 在目标对象中调用自己类内部实现的方法时, 这些调用并不会转发到代理对象中, 甚至代理对象都不知道有此调用的存在.
image.png
因此此处的add和update代表的是this对象,而不是代理对象
可以参考以下生成的反编译代理类,代理方法中的自调用方法是不会再次被代理的
https://blog.csdn.net/luzhensmart/article/details/84866599
解决方案
1、开启expose-proxy
spring中expose-proxy的作用与原理
作用:将代理对象暴露出来,可以使用使用AopContext.currentProxy()获取当前代理
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
2、修改自调用方法为代理方法
((BaseController)AopContext.currentProxy()).update
image.png
网友评论