一、SpringEvent 事件监听
SpringUtils.context().publishEvent(testDemo); 上下文监事件是同步的,如果EventListen 中报错则会阻塞,不继续执行。
事件机制监听的方式有两种:此处用注解演示
- 实现ApplicationListener接口
- EventListener注解形式
//发送代码:
@Slf4j
@RestController
@RequiredArgsConstructor
public class SpringEventTestController {
@RequestMapping("send")
@SaIgnore
public R sendInfo(){
TestDemo testDemo = new TestDemo();
testDemo.setId(1L);
testDemo.setUserId(1L);
testDemo.setValue("send");
log.info("----------------->发送请求");
SpringUtils.context().publishEvent(testDemo);
return R.ok();
}
}
//监听代码
@Slf4j
//1-必须交给spring管理
@Component
public class EventListen {
@Order(1)
@EventListener
public void listen1(TestDemo demo){
log.info("demo--------->{}", demo);
}
@Order(2)
@EventListener
public void listen2(TestDemo demo){
log.info("demo--------->{}", demo);
}
}
结果:
XNIO-1 task-1 表示线程号,可以看到都在同一个线程
2024-05-15 11:03:21 [XNIO-1 task-1] INFO c.r.d.c.s.SpringEventTestController
- ----------------->发送请求
2024-05-15 11:03:21 [XNIO-1 task-1] INFO c.r.d.c.sendEvent.EventListen
- demo1111--------->TestDemo(id=1, deptId=null, userId=1, orderNum=null, testKey=null, value=send, version=null, delFlag=null)
2024-05-15 11:03:21 [XNIO-1 task-1] INFO c.r.d.c.sendEvent.EventListen
- demo2222222--------->TestDemo(id=1, deptId=null, userId=1, orderNum=null, testKey=null, value=send, version=null, delFlag=null)
2024-05-15 11:03:21 [XNIO-1 task-1] INFO c.r.f.i.PlusWebInvokeTimeInterceptor
2-异步
若果想要异步执行,加上 @Async 注解即可
注意:使用异步线程的方法拿不到主线程 LoginHelper的登录信息
@Order(2)
@EventListener
@Async
public void listen2(TestDemo demo){
log.info("demo2222222--------->{}", demo);
}
结果:
打印demo2222222 的线程为 schedule-pool-1,与其他线程 XNIO-1 task-1 不一致
2024-05-15 11:07:05 [XNIO-1 task-1] INFO c.r.d.c.s.SpringEventTestController
- ----------------->发送请求
2024-05-15 11:07:05 [XNIO-1 task-1] INFO c.r.d.c.sendEvent.EventListen
- demo1111--------->TestDemo(id=1, deptId=null, userId=1, orderNum=null, testKey=null, value=send, version=null, delFlag=null)
2024-05-15 11:07:05 [schedule-pool-1] INFO c.r.d.c.sendEvent.EventListen
- demo2222222--------->TestDemo(id=1, deptId=null, userId=1, orderNum=null, testKey=null, value=send, version=null, delFlag=null)
2024-05-15 11:07:05 [XNIO-1 task-1] INFO c.r.f.i.PlusWebInvokeTimeInterceptor
3- 事务TransactionalEventListener
TransactionalEventListener 提供四种模式 默认是AFTER_COMMIT,在提交之后
代码示例:
//发送
@RequestMapping("sendError")
@Transactional
@SaIgnore
public void sendError() {
//更新事务
iTestDemoService.queryById(1L);
TestDemo testDemo = new TestDemo();
testDemo.setId(1L);
log.info("----------------->发送请求");
SpringUtils.context().publishEvent(testDemo);
//抛出异常触发回滚
throw new ServiceException("error啦,回滚上面事务吧。。。。。。。");
}
//监听
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void listen3(TestDemo demo){
log.info("事务提交成功--------->{}", demo);
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void listen4(TestDemo demo){
log.info("触发事务回滚--------->{}", demo);
}
结果:
TransactionPhase.BEFORE_COMMIT 执行了,而listen3 事务提交成功未触发
2024-05-15 11:45:31 [XNIO-1 task-1] INFO c.r.d.c.s.SpringEventTestController
- ----------------->发送请求
2024-05-15 11:45:31 [XNIO-1 task-1] INFO c.r.d.c.sendEvent.EventListen
- 触发事务回滚--------->TestDemo(id=1, deptId=null, userId=null, orderNum=null, testKey=null, value=null, version=null, delFlag=null)
2024-05-15 11:45:31 [XNIO-1 task-1] ERROR c.r.f.w.e.GlobalExceptionHandler
- error啦,回滚上面事务吧。。。。。。。
2024-05-15 11:45:31 [XNIO-1 task-1] INFO c.r.f.i.PlusWebInvokeTimeInterceptor
二、登录日志
1-路径:com.ruoyi.system.service.SysLoginService
中 recordLogininfor 方法通过监听事件记录登录信息
private void recordLogininfor(String username, String status, String message) { LogininforEvent logininforEvent = new LogininforEvent(); logininforEvent.setUsername(username); logininforEvent.setStatus(status); logininforEvent.setMessage(message); logininforEvent.setRequest(ServletUtils.getRequest()); SpringUtils.context().publishEvent(logininforEvent); }
2-跳转到监听具体实现:com.ruoyi.system.service.impl.SysLogininforServiceImpl
该处用 @Async、@EventListener 2个注解实现
注意:@Async 异步,监听recordLogininfor方法则会在新线程执行
所以logininforEvent中设置了请求信息:logininforEvent.setRequest(ServletUtils.getRequest());@Async @EventListener public void recordLogininfor(LogininforEvent logininforEvent) { HttpServletRequest request = logininforEvent.getRequest(); final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent")); final String ip = ServletUtils.getClientIP(request); String address = AddressUtils.getRealAddressByIP(ip); StringBuilder s = new StringBuilder(); s.append(getBlock(ip)); s.append(address); s.append(getBlock(logininforEvent.getUsername())); s.append(getBlock(logininforEvent.getStatus())); s.append(getBlock(logininforEvent.getMessage())); // 打印信息到日志 log.info(s.toString(), logininforEvent.getArgs()); // 获取客户端操作系统 String os = userAgent.getOs().getName(); // 获取客户端浏览器 String browser = userAgent.getBrowser().getName(); // 封装对象 SysLogininfor logininfor = new SysLogininfor(); logininfor.setUserName(logininforEvent.getUsername()); logininfor.setIpaddr(ip); logininfor.setLoginLocation(address); logininfor.setBrowser(browser); logininfor.setOs(os); logininfor.setMsg(logininforEvent.getMessage()); // 日志状态 if (StringUtils.equalsAny(logininforEvent.getStatus(), Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) { logininfor.setStatus(Constants.SUCCESS); } else if (Constants.LOGIN_FAIL.equals(logininforEvent.getStatus())) { logininfor.setStatus(Constants.FAIL); } // 插入数据 insertLogininfor(logininfor); }
三、@Log 日志注解
1-如下cortroller中标注了@Log 注解
@SaCheckPermission("monitor:logininfor:unlock")
@Log(title = "账户解锁", businessType = BusinessType.OTHER)
@GetMapping("/unlock/{userName}")
public R<Void> unlock(@PathVariable("userName") String userName) {
String loginName = CacheConstants.PWD_ERR_CNT_KEY + userName;
if (RedisUtils.hasKey(loginName)) {
RedisUtils.deleteObject(loginName);
}
return R.ok();
}
2-根据注解找到其切面类: com.ruoyi.framework.aspectj.LogAspect
如下: @AfterReturning(pointcut = "@annotation(controllerLog)" ,指切 Log 标注的方法
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object
package com.ruoyi.framework.aspectj;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.event.OperLogEvent;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.BusinessStatus;
import com.ruoyi.common.enums.HttpMethod;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.JsonUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/**
* 操作日志记录处理
*
* @author Lion Li
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// *========数据库日志=========*//
OperLogEvent operLog = new OperLogEvent();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = ServletUtils.getClientIP();
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
LoginUser loginUser = LoginHelper.getLoginUser();
operLog.setOperName(loginUser.getUsername());
operLog.setDeptName(loginUser.getDeptName());
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 发布事件保存数据库
SpringUtils.context().publishEvent(operLog);
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog, log.excludeParamNames());
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
String requestMethod = operLog.getRequestMethod();
if (MapUtil.isEmpty(paramsMap)
&& HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = JsonUtils.toJsonString(o);
Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
MapUtil.removeAny(dict, excludeParamNames);
str = JsonUtils.toJsonString(dict);
}
params.add(str);
}
}
return params.toString();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
3-日志事件监听类如下:
主要是保存数据入库
/**
* 操作日志记录
*
* @param operLogEvent 操作日志事件
*/
@Async
@EventListener
public void recordOper(OperLogEvent operLogEvent) {
SysOperLog operLog = BeanUtil.toBean(operLogEvent, SysOperLog.class);
// 远程查询操作地点
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
insertOperlog(operLog);
}