个人技术分享

一、SpringEvent 事件监听

 SpringUtils.context().publishEvent(testDemo);  上下文监事件是同步的,如果EventListen  中报错则会阻塞,不继续执行。

事件机制监听的方式有两种:此处用注解演示

  1. 实现ApplicationListener接口
  2. 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);
    }