小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
本节介绍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
来获得当前的request
和response
,RequestContextHolder
一般都是用来帮助在Service
层中获得request
和response
的,毕竟从controller
中传入service
中request
和response
有点过于简单粗暴
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");
NamedThreadLocal
和NamedInheritableThreadLocal
都继承自ThreadLocal
,NamedThreadLocal
就是多出来一个名字,但是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的?
是在webmvc
包的FrameworkServlet
文件中实现的,FrameworkServlet
中重写了doGet等方法在这些方法中使用了processRequest方法,在这个方法中可以看到进行了当前的request
和response
的设置。
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)也能够重置表的主键,显然是一个好选择,清空必不可少,因为如果登录日志过多,这个表的查询肯定会越来越慢。