Springboot公共字段填充及ThreadLocal模塊改進(jìn)方案
1. 公共字段自動(dòng)填充
1.1 問(wèn)題分析
在新增員工時(shí)需要設(shè)置創(chuàng)建時(shí)間、創(chuàng)建人、修改時(shí)間、修改人等字段,在編輯員工時(shí)需要設(shè)置修改時(shí)間、修改人等字段。這些字段屬于公共字段,也就是也就是在系統(tǒng)中很多表中都會(huì)有這些字段,如下:

而針對(duì)于這些字段的賦值方式為:
A. 在新增數(shù)據(jù)時(shí), 將createTime、updateTime 設(shè)置為當(dāng)前時(shí)間, createUser、updateUser設(shè)置為當(dāng)前登錄用戶ID。
B. 在更新數(shù)據(jù)時(shí), 將updateTime 設(shè)置為當(dāng)前時(shí)間, updateUser設(shè)置為當(dāng)前登錄用戶ID。
目前,在項(xiàng)目中處理這些字段都是在每一個(gè)業(yè)務(wù)方法中進(jìn)行賦值操作,如下


如果都按照上述的操作方式來(lái)處理這些公共字段, 需要在每一個(gè)業(yè)務(wù)方法中進(jìn)行操作, 編碼相對(duì)冗余、繁瑣。
改進(jìn):使用Mybatis Plus提供的公共字段自動(dòng)填充功能來(lái)簡(jiǎn)化開(kāi)發(fā)。
1.2 基本功能實(shí)現(xiàn)
1.2.1 思路分析
Mybatis Plus公共字段自動(dòng)填充,即在插入或者更新的時(shí)候?yàn)橹付ㄗ侄钨x予指定的值,好處統(tǒng)一對(duì)這些字段進(jìn)行處理,避免了重復(fù)代碼。在上述的問(wèn)題分析中,有四個(gè)公共字段需要在新增/更新中進(jìn)行賦值操作, 具體情況如下:
| 字段名 | 賦值時(shí)機(jī) | 說(shuō)明 |
|---|---|---|
| createTime | 插入(INSERT) | 當(dāng)前時(shí)間 |
| updateTime | 插入(INSERT) , 更新(UPDATE) | 當(dāng)前時(shí)間 |
| createUser | 插入(INSERT) | 當(dāng)前登錄用戶ID |
| updateUser | 插入(INSERT) , 更新(UPDATE) |
實(shí)現(xiàn)步驟:
1、在實(shí)體類的屬性上加入@TableField注解,指定自動(dòng)填充的策略。
2、按照框架要求編寫元數(shù)據(jù)對(duì)象處理器,在此類中統(tǒng)一為公共字段賦值,此類需要實(shí)現(xiàn)MetaObjectHandler接口。
1.3 代碼實(shí)現(xiàn)
1). 實(shí)體類的屬性上加入@TableField注解,指定自動(dòng)填充的策略。
在員工Employee實(shí)體類的公共字段屬性上, 加上注解, 指定填充策略。
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**@Description: 員工實(shí)體類 該實(shí)體類主要用于和員工表 employee 進(jìn)行映射。
* @version v1.0
* @author LiBiGo
* @date 2022/8/12 11:05
*/
// 在實(shí)體類上添加@Data注解,可以省去代碼中大量的 get()、 set()、 toString() 等方法,提高代碼的簡(jiǎn)潔:
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
// map-underscore-to-camel-case: true
// 在映射實(shí)體或者屬性時(shí),將數(shù)據(jù)庫(kù)中表名和字段名中的下劃線去掉,按照駝峰命名法映射
private String idNumber; //身份證 因?yàn)樵谂渲梦募性O(shè)置駝峰命名,所以與數(shù)據(jù)庫(kù)中的不太一樣,數(shù)據(jù)庫(kù)中為id_number
private Integer status;
@TableField(fill = FieldFill.INSERT) // 插入時(shí)填充該屬性值
private LocalDateTime createTime; // 同上 駝峰命名法
@TableField(fill = FieldFill.INSERT_UPDATE) //插入、更新時(shí)填充該屬性值
private LocalDateTime updateTime; // 同上 駝峰命名法
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新時(shí)填充
private Long updateUser;
}
2). 按照框架要求編寫元數(shù)據(jù)對(duì)象處理器,在此類中統(tǒng)一為公共字段賦值,此類需要實(shí)現(xiàn)MetaObjectHandler接口。
package com.itheima.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* Description: 自定義元數(shù)據(jù)對(duì)象處理器|統(tǒng)一為公共字段賦值,此類需要實(shí)現(xiàn)MetaObjectHandler接口。
* @author w
* @version 1.0
* @date 2022/8/15 12:05
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
/**@Description: 插入操作自動(dòng)填充
* @version v1.0
* @author LiBiGo
* @date 2022/8/15 12:14
*/
log.info("公共字段自動(dòng)填充insert...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId()); // 使用了Thread 看后續(xù)
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
/**@Description: 更新操作自動(dòng)填充
* @version v1.0
* @author LiBiGo
* @date 2022/8/15 12:14
*/
log.info("公共字段自動(dòng)填充update...");
log.info(metaObject.toString());
long id = Thread.currentThread().getId();
log.info("線程ID:{}",id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
2 使用ThreadLocal對(duì)公共字段填充功能進(jìn)行完善
2.1 思路分析
2.1.1 提出設(shè)想
前面已經(jīng)完成公共字段自動(dòng)填充功能的代碼開(kāi)發(fā),但是還有一個(gè)問(wèn)題沒(méi)有解決,就是在自動(dòng)填充createUser和updateUser時(shí)設(shè)置的用戶id是固定值,現(xiàn)在需要改造成動(dòng)態(tài)獲取當(dāng)前登錄用戶的id。
提出設(shè)想:用戶登錄成功后將用戶id存入了HttpSession中,現(xiàn)在從HttpSession中獲取不就行了?

存在問(wèn)題:在MyMetaObjectHandler類中是不能直接獲得HttpSession對(duì)象的,所以需要通過(guò)其他方式來(lái)獲取登錄用戶id。
2.1.2 分析問(wèn)題
在修改員工信息時(shí), 業(yè)務(wù)的執(zhí)行流程如下圖:

客戶端發(fā)送的每次http請(qǐng)求,對(duì)應(yīng)的在服務(wù)端都會(huì)分配一個(gè)新的線程來(lái)處理,在處理過(guò)程中涉及到下面類中的方法都屬于相同的一個(gè)線程:
1). LoginCheckFilter的doFilter方法
2). EmployeeController的update方法
3). MyMetaObjectHandler的updateFill方法
我們可以在上述類的方法中加入如下代碼(獲取當(dāng)前線程ID,并輸出):
long id = Thread.currentThread().getId();
log.info("線程id為:{}",id);
執(zhí)行編輯員工功能進(jìn)行驗(yàn)證,通過(guò)觀察控制臺(tái)輸出可以發(fā)現(xiàn),一次請(qǐng)求對(duì)應(yīng)的線程id是相同的:

經(jīng)過(guò)上述的分析之后,發(fā)現(xiàn)可以使用JDK提供的一個(gè)ThreadLocal類來(lái)解決此問(wèn)題。
2.2 ThreadLocal
2.2.1 ThreadLocal基本概述
ThreadLocal并不是一個(gè)Thread,而是Thread的局部變量。
當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。
ThreadLocal為每個(gè)線程提供單獨(dú)一份存儲(chǔ)空間,具有線程隔離的效果,只有在線程內(nèi)才能獲取到對(duì)應(yīng)的值,線程外則不能訪問(wèn)當(dāng)前線程對(duì)應(yīng)的值。
2.2.2 ThreadLocal常用方法
A. public void set(T value) : 設(shè)置當(dāng)前線程的線程局部變量的值
B. public T get() : 返回當(dāng)前線程所對(duì)應(yīng)的線程局部變量的值
C. public void remove() : 刪除當(dāng)前線程所對(duì)應(yīng)的線程局部變量的值
我們可以在LoginCheckFilter的doFilter方法中獲取當(dāng)前登錄用戶id,并調(diào)用ThreadLocal的set方法來(lái)設(shè)置當(dāng)前線程的線程局部變量的值(用戶id),然后在MyMetaObjectHandler的updateFill方法中調(diào)用ThreadLocal的get方法來(lái)獲得當(dāng)前線程所對(duì)應(yīng)的線程局部變量的值(用戶id)。 如果在后續(xù)的操作中, 我們需要在Controller / Service中要使用當(dāng)前登錄用戶的ID, 可以直接從ThreadLocal直接獲取。
2.3 操作步驟
1). 編寫B(tài)aseContext工具類,基于ThreadLocal封裝的工具類
2). 在LoginCheckFilter的doFilter方法中調(diào)用BaseContext來(lái)設(shè)置當(dāng)前登錄用戶的id
3). 在MyMetaObjectHandler的方法中調(diào)用BaseContext獲取登錄用戶的id
2.4 代碼實(shí)現(xiàn)
1). BaseContext工具類
package com.itheima.reggie.common;
/**
* Description: 基于ThreadLocal封裝工具類,用戶保存和獲取當(dāng)前登陸用戶ID
* @date 2022/8/15 12:40
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 設(shè)置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 獲取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
2).LoginCheckFilter中存放當(dāng)前登錄用戶到ThreadLocal
在doFilter方法中, 判定用戶是否登錄, 如果用戶登錄, 在放行之前, 獲取HttpSession中的登錄用戶信息, 調(diào)用BaseContext的setCurrentId方法將當(dāng)前登錄用戶ID存入ThreadLocal。
package com.itheima.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.BaseContext;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Description: 檢查用戶是否已經(jīng)完成登陸
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*") //設(shè)置攔截器,設(shè)置攔截的網(wǎng)頁(yè)區(qū)域
@Slf4j
public class LoginCheckFilter implements Filter {
// 路徑匹配器,支持通配符,因?yàn)橄旅娴男蛄惺褂昧送ㄅ浞?
// AntPathMatcher匹配規(guī)則 ? 匹配一個(gè)字符 * 匹配0個(gè)或多個(gè)字符 ** 匹配0個(gè)或多個(gè)目錄/字符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
// A. 獲取本次請(qǐng)求的URI
String requestURI = request.getRequestURI();
log.info("攔截的請(qǐng)求:{}",requestURI);
// 定義不攔截的序列,只攔截頁(yè)面數(shù)據(jù)請(qǐng)求,不攔截頁(yè)面格式
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// B. 判斷本次請(qǐng)求, 是否需要登錄, 才可以訪問(wèn)
boolean check = check(urls, requestURI);
// C. 如果不需要,則直接放行
if(check){
log.info("本次請(qǐng)求{}不需要處理",requestURI);
filterChain.doFilter(request,response);
return;
}
// D. 判斷登錄狀態(tài),如果已登錄,則直接放行
if(request.getSession().getAttribute("employee")!=null){
log.info("用戶已登錄,用戶id為:{}",request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee"); // 獲取當(dāng)前用戶登陸id
BaseContext.setCurrentId(empId); //設(shè)置線程共享
filterChain.doFilter(request,response); // 補(bǔ)課 不清楚????????????
long id = Thread.currentThread().getId();
log.info("檢測(cè)是否登陸線程ID:{}",id);
return;
}
// E. 如果未登錄, 則返回未登錄結(jié)果
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
public boolean check(String[] urls,String requestURI){
/**@Description: 路徑匹配,檢查本次請(qǐng)求是否需要放行
* @author LiBiGo
* @date 2022/8/12 16:50
*/
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI); // 使用通配符對(duì)象,配對(duì)資源
if(match){
return true;
}
}
return false;
}
}
3). MyMetaObjectHandler中從ThreadLocal中獲取
將之前在代碼中添加動(dòng)態(tài)調(diào)用BaseContext中的getCurrentId方法獲取當(dāng)前登錄用戶ID

3 功能測(cè)試
編寫完了元數(shù)據(jù)對(duì)象處理器之后,我們就可以將之前在新增和修改方法中手動(dòng)賦值的代碼刪除或注釋掉。


完善了元數(shù)據(jù)對(duì)象處理器重新啟動(dòng)項(xiàng)目,完成登錄操作后,在員工管理模塊中,測(cè)試增加/更新員工信息功能,直接查詢數(shù)據(jù)庫(kù)數(shù)據(jù)變更,看看我們?cè)谛略?修改數(shù)據(jù)時(shí),這些公共字段數(shù)據(jù)是否能夠完成自動(dòng)填充, 并且看看填充的create_user 及 update_user字段值是不是本地登錄用戶的ID。
以上就是Springboot公共字段填充及ThreadLocal模塊改進(jìn)方案的詳細(xì)內(nèi)容,更多關(guān)于Springboot 公共字段填充的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- springboot在filter中如何用threadlocal存放用戶身份信息
- SpringBoot中的ThreadLocal保存請(qǐng)求用戶信息的實(shí)例demo
- springboot登錄攔截器+ThreadLocal實(shí)現(xiàn)用戶信息存儲(chǔ)的實(shí)例代碼
- SpringBoot ThreadLocal 簡(jiǎn)單介紹及使用詳解
- SpringBoot+ThreadLocal+AbstractRoutingDataSource實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源
- SpringBoot ThreadLocal實(shí)現(xiàn)公共字段自動(dòng)填充案例講解
- SpringBoot通過(guò)ThreadLocal實(shí)現(xiàn)登錄攔截詳解流程
- springboot 使用ThreadLocal的實(shí)例代碼
- SpringBoot中使用?ThreadLocal?進(jìn)行多線程上下文管理及注意事項(xiàng)小結(jié)
相關(guān)文章
Java基于裝飾者模式實(shí)現(xiàn)的圖片工具類實(shí)例【附demo源碼下載】
這篇文章主要介紹了Java基于裝飾者模式實(shí)現(xiàn)的圖片工具類,結(jié)合完整實(shí)例形式分析了裝飾者模式實(shí)現(xiàn)圖片的判斷、水印、縮放、復(fù)制等功能,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2017-09-09
從JVM的內(nèi)存管理角度分析Java的GC垃圾回收機(jī)制
這篇文章主要介紹了從JVM的內(nèi)存管理角度分析Java的GC垃圾回收機(jī)制,帶有GC是Java語(yǔ)言的重要特性之一,需要的朋友可以參考下2015-11-11
Javaweb開(kāi)發(fā)環(huán)境Myeclipse6.5 JDK1.6 Tomcat6.0 SVN1.8配置教程
這篇文章主要介紹了Javaweb開(kāi)發(fā)環(huán)境Myeclipse6.5 JDK1.6 Tomcat6.0 SVN1.8配置教程,感興趣的小伙伴們可以參考一下2016-06-06
細(xì)數(shù)java for循環(huán)中的那些坑
這篇文章主要介紹了Java for循環(huán)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07

