阅读 105

从RuoYi-Vue 的登录日志模块看RequestContextHolder

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

本节介绍RuoYi-Vue的ruoyi-admin模块中的登录日志模块SysLogininforController 部分的代码,这个接口主要用来展示用户登录日志的情况,同时也讲解RequestContextHolder的一些相关知识。SysOperlogController与之相似,所以也就不做赘述了。

列表页面


    @GetMapping("/list")

    public TableDataInfo list(SysLogininfor logininfor) {

        startPage();

        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);

        return getDataTable(list);

    }

复制代码

这是系统里面比较常见的列表页面的实现方式,参数是SysLogininfor,返回的对象是

TableDataInfo,里面包裹的是List<SysLogininfor>。下面来看看具体的实现

startPage

这是继承BaseController时得到的方法,使用了pagehelper来实现分页操作。

/**

     * 设置请求分页数据

     */

    protected void startPage() {

        PageDomain pageDomain = TableSupport.buildPageRequest();

        Integer pageNum = pageDomain.getPageNum();

        Integer pageSize = pageDomain.getPageSize();

        if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) {

            String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());

            Boolean reasonable = pageDomain.getReasonable();

            PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);

        }

    }

复制代码

这里是用来从参数中获取分页参数的方法,那么在Common模块里面的TableSupport如何拿到传入的参数的呢?毕竟这里并没有传入对应的Request啊? 其实很简单,TableSupport.buildPageRequest 这里通过RequestContextHolder来获得当前的requestresponseRequestContextHolder一般都是用来帮助在Service层中获得requestresponse的,毕竟从controller中传入servicerequestresponse有点过于简单粗暴


    public static ServletRequestAttributes getRequestAttributes()
    {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }
    /**
     * 获取request
     */
    public static HttpServletRequest getRequest()
    {
        return getRequestAttributes().getRequest();
    }
    /**
     * 获取response
     */
    public static HttpServletResponse getResponse()
    {
        return getRequestAttributes().getResponse();
    }
复制代码

在它的帮助下我们就可以很轻松的将对应的请求和响应拿到了。

RequestContextHolder的 疑问

怎么知道的当前请求的request和response是啥?

RequestContextHolder这个类中,我们可以看到两个继承自ThreadLocal的子类保存当前线程下的request

    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
复制代码

NamedThreadLocalNamedInheritableThreadLocal都继承自ThreadLocalNamedThreadLocal就是多出来一个名字,但是inheritableRequestAttributesHolder是比较有意思的,它对应的NamedInheritableThreadLocal代表其可以被子线程继承。

    public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        } else if (inheritable) {
            inheritableRequestAttributesHolder.set(attributes);
            requestAttributesHolder.remove();
        } else {
            requestAttributesHolder.set(attributes);
            inheritableRequestAttributesHolder.remove();
        }
    }
    @Nullable
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
        if (attributes == null) {
            attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
        }
        return attributes;
    }
复制代码

从上面的get方法也可以看出这一点,先取本线程,不存在再取父线程的。

什么时候设置进去当前的request和response的?

image.png

是在webmvc包的FrameworkServlet文件中实现的,FrameworkServlet中重写了doGet等方法在这些方法中使用了processRequest方法,在这个方法中可以看到进行了当前的requestresponse的设置。

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        //获取上一个请求保存的LocaleContext
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        //当前的LocaleContext
        LocaleContext localeContext = this.buildLocaleContext(request);
        //上一个请求保存的RequestAttributes 
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        //建立新的RequestAttributes
        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
        //在此进行具体的设置
        this.initContextHolders(request, localeContext, requestAttributes);
        try {
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
            failureCause = var17;
            throw new NestedServletException("Request processing failed", var17);
        } finally {
            //进行恢复
            this.resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }
            this.logResult(request, response, (Throwable)failureCause, asyncManager);
            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
        }
    }
复制代码

具体的设置方法中,我们可以看到调用了我们上面展示的setRequestAttributes方法,在这里进行了请求上下文的设置。

     private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
        if (localeContext != null) {
            LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
        }
        if (requestAttributes != null) {
            RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
        }
    }
复制代码

数据库设计

可以看到这个系统登录日志表

drop table if exists sys_logininfor;

create table sys_logininfor (
  info_id        bigint(20)     not null auto_increment   comment '访问ID',
  user_name      varchar(50)    default ''                comment '用户账号',
  ipaddr         varchar(128)   default ''                comment '登录IP地址',
  login_location varchar(255)   default ''                comment '登录地点',
  browser        varchar(50)    default ''                comment '浏览器类型',
  os             varchar(50)    default ''                comment '操作系统',
  status         char(1)        default '0'               comment '登录状态(0成功 1失败)',
  msg            varchar(255)   default ''                comment '提示消息',
  login_time     datetime                                 comment '访问时间',
  primary key (info_id)

) engine=innodb auto_increment=100 comment = '系统访问记录';

复制代码

中除了一个主键没有其他的索引,实际上我觉得可以加一个时间的索引,因为平常搜日志的时候一般都会加时间去卡数据量。

另外我们可以看到最后的清空数据这个接口,它使用的是truncate 

    <update id="cleanLogininfor">
        truncate table sys_logininfor
    </update>
复制代码

相较于delete的一条条删除,truncate 更快(毕竟是drop之后重新create)也能够重置表的主键,显然是一个好选择,清空必不可少,因为如果登录日志过多,这个表的查询肯定会越来越慢。

文章分类
后端
文章标签