经典面试题--如何进行登录状态的日志记录

3 阅读1分钟

面试过程中经常会被问到:对于需要记录某个用户的登录行为时,你是怎么设计的?

对于上述问题此处做一下汇总:

使用SpringMVC的intercept

  1. 实现HandlerIntercept类, 重写preHandle、afterCompletion方法
  2. 新建WebConfig类,实现WebMvcConfigurer,重写addInterceptors方法,同时需要使用注解@Configuration

示例如下:

@Component
public class ApiMonitorInterceptor implements HandlerInterceptor {
     
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 业务逻辑
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) {
        // 业务逻辑
    }
}

package com.fakewang.demo01.config;

import com.fakewang.demo01.interceptor.ApiMonitorInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private ApiMonitorInterceptor apiMonitorInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiMonitorInterceptor).addPathPatterns("/**");
    }
}

使用Spring框架的监听事件

public class RequestTimeEventListener implements ApplicationListener<ServletRequestHandledEvent> {

    private static final Logger log = LoggerFactory.getLogger(RequestTimeEventListener.class);

    @Override
    public void onApplicationEvent(ServletRequestHandledEvent event) {
        String address = event.getClientAddress();
        String method = event.getMethod();
        String url = event.getRequestUrl();
        long elapsed = event.getProcessingTimeMillis();
        Throwable cause = event.getFailureCause();
        String errorMessage = cause == null ? "" : cause.getMessage();

        if (null == cause) {
            // 请求成功:输出 INFO 级别日志
            log.info("address={}, url={}, method={}, costTime={}ms",
                   address , url, method, elapsed);
        } else {
            // 请求失败:输出 ERROR 级别日志,包含异常信息
            log.error("address={}, url={}, method={}, costTime={}ms, error={}",
                    address, url, method, elapsed, errorMessage);
        }
    }
}

使用切面

@Aspect
@Component
public class ApiMonitorAspect {

    @Pointcut("execution(* com.fakewang.demo01.controller.MainController.*(..))")
    public void controllerPointCut(){}

    @Around("controllerPointCut()")
    public Object monitorApi (ProceedingJoinPoint joinPoint) throws Throwable {}

}

使用自定义注解

定义:

package com.fakewang.demo01.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
    String value() default "";
}

切面:

public class xxx {
    // 注意这里使用的是@annotation
    @Pointcut("@annotation(com.fakewang.demo01.annotation.Monitored)")
    public void annotationPonitcut(){}

    // 注意这里和上面的不一样
    @Around("annotationPonitcut() && @annotation(monitored)")
    public Object monitorAnnotationPointcut(ProceedingJoinPoint joinPoint, Monitored monitored) throws Throwable {}
}

使用:

@Monitored("用户登录接口") // 标记需要监控的方法
@RequestMapping("/put")
public String put (@RequestBody LoginDTO dto) {
    return "登录成功";
}

使用Micrometer和Spring Actuator,搭配Prometheus + Grafana实现监控的可视化

需要搭建相应环境,有一定的资源需求