前陣子和朋友聊天,他說他們項目有個需求,要實現主鍵自動生成,不想每次新增的時候,都手動設置主鍵。于是我就問他,那你們數據庫表設置主鍵自動遞增不就得了。他的回答是他們項目目前的id都是采用雪花算法來生成,因此為了項目穩定性,不會切換id的生成方式。
朋友問我有沒有什么實現思路,他們公司的orm框架是mybatis,我就建議他說,不然讓你老大把mybatis切換成mybatis-plus。mybatis-plus就支持注解式的id自動生成,而且mybatis-plus只是對mybatis進行增強不做改變。朋友還是那句話,說為了項目穩定,之前項目組沒有使用mybatis-plus的經驗,貿然切換不知道會不會有什么坑。后面沒招了,我就跟他說不然你用mybatis的攔截器實現一個吧。于是又有一篇吹水的創作題材出現。
在介紹如何通過mybatis攔截器實現主鍵自動生成之前,我們先來梳理一些知識點
1、mybatis攔截器的作用
mybatis攔截器設計的初衷就是為了供用戶在某些時候可以實現自己的邏輯而不必去動mybatis固有的邏輯
2、Interceptor攔截器
每個自定義攔截器都要實現
org.apache.ibatis.plugin.Interceptor
這個接口,并且自定義攔截器類上添加@Intercepts注解
3、攔截器能攔截哪些類型
Executor:攔截執行器的方法。
ParameterHandler:攔截參數的處理。
ResultHandler:攔截結果集的處理。
StatementHandler:攔截Sql語法構建的處理。
4、攔截的順序
a、不同類型攔截器的執行順序
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
b、多個攔截器攔截同種類型同一個目標方法,執行順序是后配置的攔截器先執行
比如在mybatis配置如下
<plugins>
<plugin interceptor="com.lybgeek.InterceptorA" />
<plugin interceptor="com.lybgeek.InterceptorB" />
</plugins>
則InterceptorB先執行。
如果是和spring做了集成,先注入spring ioc容器的攔截器,則后執行。比如有個mybatisConfig,里面有如下攔截器bean配置
@Bean
public InterceptorA interceptorA(){
return new InterceptorA();
}
@Bean
public InterceptorB interceptorB(){
return new InterceptorB();
}
則InterceptorB先執行。當然如果你是直接用@Component注解這形式,則可以配合@Order注解來控制加載順序
5、攔截器注解介紹
@Intercepts:標識該類是一個攔截器
@Signature:指明自定義攔截器需要攔截哪一個類型,哪一個方法。
@Signature注解屬性中的type表示對應可以攔截四種類型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的一種;method表示對應類型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的哪類方法;args表示對應method中的參數類型
6、攔截器方法介紹
a、 intercept方法
public Object intercept(Invocation invocation) throws Throwable
這個方法就是我們來執行我們自己想實現的業務邏輯,比如我們的主鍵自動生成邏輯就是在這邊實現。
Invocation這個類中的成員屬性target就是@Signature中的type;method就是@Signature中的method;args就是@Signature中的args參數類型的具體實例對象
b、 plugin方法
public Object plugin(Object target)
這個是用返回代理對象或者是原生代理對象,如果你要返回代理對象,則返回值可以設置為
Plugin.wrap(target, this);
this為攔截器
如果返回是代理對象,則會執行攔截器的業務邏輯,如果直接返回target,就是沒有攔截器的業務邏輯。說白了就是告訴mybatis是不是要進行攔截,如果要攔截,就生成代理對象,不攔截是生成原生對象
c、 setProperties方法
public void setProperties(Properties properties)
用于在Mybatis配置文件中指定一些屬性
主要攔截
`Executor#update(MappedStatement ms, Object parameter)`}
這個方法。mybatis的insert、update、delete都是通過這個方法,因此我們通過攔截這個這方法,來實現主鍵自動生成。其代碼塊如下
@Intercepts(value={@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class})})
public class AutoIdInterceptor implements Interceptor {}
Executor 提供的方法中,update 包含了 新增,修改和刪除類型,無法直接區分,需要借助 MappedStatement 類的屬性 SqlCommandType 來進行判斷,該類包含了所有的操作類型
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}
當SqlCommandType類型是insert我們才進行主鍵自增操作
3.1、編寫自動生成id注解
Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
/**
* 主鍵名
* @return
*/
String primaryKey();
/**
* 支持的主鍵算法類型
* @return
*/
IdType type() default IdType.SNOWFLAKE;
enum IdType{
SNOWFLAKE
}
}
3.2、 雪花算法實現
我們可以直接拿hutool這個工具包提供的idUtil來直接實現算法。
引入
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
Snowflake snowflake=IdUtil.createSnowflake(0,0);
long value=snowflake.nextId();
3.3、填充主鍵值
其實現核心是利用反射。其核心代碼片段如下
ReflectionUtils.doWithFields(entity.getClass(), field->{
ReflectionUtils.makeAccessible(field);
AutoId autoId=field.getAnnotation(AutoId.class);
if(!ObjectUtils.isEmpty(autoId) && (field.getType().isAssignableFrom(Long.class))){
switch (autoId.type()){
case SNOWFLAKE:
SnowFlakeAutoIdProcess snowFlakeAutoIdProcess=new SnowFlakeAutoIdProcess(field);
snowFlakeAutoIdProcess.setPrimaryKey(autoId.primaryKey());
finalIdProcesses.add(snowFlakeAutoIdProcess);
break;
}
}
});
public class SnowFlakeAutoIdProcess extends BaseAutoIdProcess {
private static Snowflake snowflake=IdUtil.createSnowflake(0,0);
public SnowFlakeAutoIdProcess(Field field) {
super(field);
}
@Override
void setFieldValue(Object entity) throws Exception{
long value=snowflake.nextId();
field.set(entity,value);
}
}
如果項目中的mapper.xml已經的insert語句已經含有id,比如
insert into sys_test( `id`,`type`, `url`,`menu_type`,`gmt_create`)values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
則只需到填充id值這一步。攔截器的任務就完成。如果mapper.xml的insert不含id,形如
insert into sys_test( `type`, `url`,`menu_type`,`gmt_create`)values( #{type}, #{url},#{menuType},#{gmtCreate})
則還需重寫insert語句以及新增id參數
4.1 重寫insert語句
方法一:
從 MappedStatement 對象中獲取 SqlSource 對象,再從從 SqlSource 對象中獲取獲取 BoundSql 對象,通過 BoundSql#getSql 方法獲取原始的sql,最后在原始sql的基礎上追加id
方法二:
引入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
通過
com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser
獲取相應的表名、需要insert的字段名。然后重新拼湊出新的insert語句
4.2 把新的sql重置給Invocation
其核心實現思路是創建一個新的MappedStatement,新的MappedStatement綁定新sql,再把新的MappedStatement賦值給Invocation的args[0],代碼片段如下
private void resetSql2Invocation(Invocation invocation, BoundSqlHelper boundSqlHelper,Object entity) throws SQLException {
final Object[] args=invocation.getArgs();
MappedStatement statement=(MappedStatement) args[0];
MappedStatement newStatement=newMappedStatement(statement, new BoundSqlSqlSource(boundSqlHelper));
MetaObject msObject=MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
msObject.setValue("sqlSource.boundSqlHelper.boundSql.sql", boundSqlHelper.getSql());
args[0]=newStatement;
}
4.3 新增id參數
其核心是利用
org.apache.ibatis.mapping.ParameterMapping
核心代碼片段如下
private void setPrimaryKeyParaterMapping(String primaryKey) {
ParameterMapping parameterMapping=new ParameterMapping.Builder(boundSqlHelper.getConfiguration(),primaryKey,boundSqlHelper.getTypeHandler()).build();
boundSqlHelper.getBoundSql().getParameterMappings().add(parameterMapping);
}
5、將mybatis攔截器注入到spring容器
可以直接在攔截器上加
@org.springframework.stereotype.Component
注解。也可以通過
@Bean
public AutoIdInterceptor autoIdInterceptor(){
return new AutoIdInterceptor();
}
6、在需要實現自增主鍵的實體字段上加如下注解
@AutoId(primaryKey="id")
private Long id;
1、對應的測試實體以及單元測試代碼如下
@Data
public class TestDO implements Serializable {
private static final long serialVersionUID=1L;
@AutoId(primaryKey="id")
private Long id;
private Integer type;
private String url;
private Date gmtCreate;
private String menuType;
}
@Autowired
private TestService testService;
@Test
public void testAdd(){
TestDO testDO=new TestDO();
testDO.setType(1);
testDO.setMenuType("1");
testDO.setUrl("www.test.com");
testDO.setGmtCreate(new Date());
testService.save(testDO);
testService.get(110L);
}
@Test
public void testBatch(){
List<TestDO> testDOList=new ArrayList<>();
for (int i=0; i < 3; i++) {
TestDO testDO=new TestDO();
testDO.setType(i);
testDO.setMenuType(i+"");
testDO.setUrl("www.test"+i+".com");
testDO.setGmtCreate(new Date());
testDOList.add(testDO);
}
testService.saveBatch(testDOList);
}
2、當mapper的insert語句中含有id,形如下
<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`)
values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
</insert>
以及批量插入sql
<insert id="saveBatch" parameterType="java.util.List" useGeneratedKeys="false">
insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`)
values
<foreach collection="list" item="test" index="index" separator=",">
( #{test.id},#{test.gmtCreate},#{test.type}, #{test.url},
#{test.menuType})
</foreach>
</insert>
查看控制臺sql打印語句
15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save -==> Preparing: insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`) values( ?,?, ?,?,? )
15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save -==> Parameters: 356829258376544258(Long), 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:52:04.738(Timestamp)
15:52:04 [main] DEBUG com.nlybgeek.dao.TestDao.save - <==Updates: 1
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch -==> Preparing: insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`) values ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?)
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch -==> Parameters: 356829258896637961(Long), 2020-09-11 15:52:04.847(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356829258896637960(Long), 2020-09-11 15:52:04.847(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356829258896637962(Long), 2020-09-11 15:52:04.847(Timestamp), 2(Integer), www.test2.com(String), 2(String)
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==Updates: 3
查看數據庫
3、當mapper的insert語句中不含id,形如下
<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
insert into sys_test(`type`, `url`,`menu_type`,`gmt_create`)
values(#{type}, #{url},#{menuType},#{gmtCreate})
</insert>
以及批量插入sql
<insert id="saveBatch" parameterType="java.util.List" useGeneratedKeys="false">
insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`)
values
<foreach collection="list" item="test" index="index" separator=",">
(#{test.gmtCreate},#{test.type}, #{test.url},
#{test.menuType})
</foreach>
</insert>
查看控制臺sql打印語句
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save -==> Preparing: insert into sys_test(`type`,`url`,`menu_type`,`gmt_create`,id) values (?,?,?,?,?)
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save -==> Parameters: 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:59:46.741(Timestamp), 356831196144992264(Long)
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - <==Updates: 1
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch -==> Preparing: insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`,id) values (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?)
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch -==> Parameters: 2020-09-11 15:59:46.845(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356831196635725829(Long), 2020-09-11 15:59:46.845(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356831196635725828(Long), 2020-09-11 15:59:46.845(Timestamp), 2(Integer), www.test2.com(String), 2(String), 356831196635725830(Long)
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==Updates: 3
從控制臺我們可以看出,當mapper.xml沒有配置id字段時,則攔截器會自動幫我們追加id字段
查看數據庫
本文雖然是介紹mybatis攔截器實現主鍵自動生成,但文中更多講解如何實現一個攔截器以及主鍵生成思路,并沒把intercept實現主鍵方法貼出來。其原因主要是主鍵自動生成在mybatis-plus里面就有實現,其次是有思路后,大家就可以自己實現了。最后對具體實現感興趣的朋友,可以查看文末中demo鏈接
https://www.cnblogs.com/chenchen127/p/12111159.html
https://blog.csdn.net/hncaoyuqi/article/details/103187983
https://blog.csdn.net/zsj777/article/details/81986096
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-mybatis-autoId
login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path=request.getContextPath();
String basePath=request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<!DOCTYPE html PUBLIC "-//W3C//D HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>登錄頁面</title>
<base href="<%=basePath %>">
<link rel="stylesheet" type="text/css" href="js/jquery-easyui-1.4.1/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="js/jquery-easyui-1.4.1/themes/icon.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<script type="text/javascript" src="js/jquery3.4.1/jquery3.4.1.min.js"></script>
<script type="text/javascript" src="js/jquery-easyui-1.4.1/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery-easyui-1.4.1/jquery.easyui.min.js"></script>
<script type="text/javascript" src="commons/validate.js"></script>
<script type="text/javascript" src="js/jquery-easyui-1.4.1/locale/easyui-lang-zh_CN.js"></script>
<script type="text/javascript" src="js/common.js"></script>
</head>
<body>
<div id="login_frame">
<img src="images/logo.png" class="logo">
<form method="post" action="/login/login" onsubmit="return check()">
<p><label class="label_input">用戶名</label><input type="text" id="username" name="username" class="text_field"/>
</p>
<p><label class="label_input">密碼</label><input type="password" id="password" name="password"
class="text_field"/></p>
<div id="login_control">
<input type="submit" id="btn_login" value="登錄"/>
<%-- <a id="forget_pwd" href="forget_pwd.html">忘記密碼?</a>--%>
</div>
</form>
</div>
</body>
</html>
<script>
function check() {
var username=$("#username").val();
var password=$("#password").val();
if (username==="" || username===null) {
alert("請輸入用戶名");
return false;
} else if (password==="" || password===null) {
alert("請輸入密碼");
return false;
} else {
return true;
}
}
</script>
<style>
body {
background-size: 100%;
background-repeat: no-repeat;
}
#login_frame {
width: 400px;
height: 260px;
padding: 13px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -200px;
margin-top: -200px;
background-color: #bed2c7;
border-radius: 10px;
text-align: center;
}
form p > * {
display: inline-block;
vertical-align: middle;
}
#image_logo {
margin-top: 22px;
}
.label_input {
font-size: 14px;
font-family: 宋體;
width: 65px;
height: 28px;
line-height: 28px;
text-align: center;
color: white;
background-color: #00303E;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.text_field {
width: 278px;
height: 28px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
border: 0;
}
#btn_login {
font-size: 14px;
font-family: 宋體;
width: 120px;
height: 28px;
line-height: 28px;
text-align: center;
color: white;
background-color: #00303E;
border-radius: 6px;
border: 0;
float: left;
}
#forget_pwd {
font-size: 12px;
color: white;
text-decoration: none;
position: relative;
float: right;
top: 5px;
}
#forget_pwd:hover {
color: blue;
text-decoration: underline;
}
#login_control {
padding: 0 28px;
}
.logo {
width: 40px;
height: 35px;
margin-top: -10px;
}
</style>
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
// 獲取請求的uri
String uri=request.getRequestURI();
// 除了login.jsp是可以公開訪問的,其它的URL都沒攔截
if (uri.indexOf("/login") >=0) {
return true;
} else {
// 獲取session
HttpSession session=request.getSession();
UserPojo user=(UserPojo) session.getAttribute("USER_SESSION");
// 判斷session中是否有用戶數據,如果有數據,則返回true。否則重定向到登錄頁面
if (user !=null) {
return true;
} else {
response.sendRedirect("/login/login");
return false;
}
}
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
小伙伴使用spring boot開發多年,但是對于過濾器和攔截器的主要區別依然傻傻分不清。今天就對這兩個概念做一個全面的盤點。
定義過濾器:實現javax.servlet.Filter接口,并重寫doFilter方法。
注冊過濾器:通過@WebFilter注解自動注冊,或者使用FilterRegistrationBean在Spring配置類中注冊。
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class SimpleCORSFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response=(HttpServletResponse) res;
HttpServletRequest request=(HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, Content-Type");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}
上面代碼定義了一的跨域資源共享(CORS)過濾器,用于處理跨域請求。它設置了允許的源、方法和頭部,并處理預檢請求,這個在開發中經常用到。
定義攔截器:實現HandlerInterceptor接口,并重寫preHandle方法。
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 獲取用戶信息,進行身份驗證
// ...
// 如果用戶未登錄或權限不足,返回false并設置響應狀態
if (!isUserAuthenticated(request)) {
response.setStatus(HttpServletResponse.FORBIDDEN);
return false;
}
// 用戶已登錄且權限足夠,放行請求
return true;
}
}
注冊攔截器:在Spring配置類中注冊攔截器,并指定攔截的路徑和順序。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
上面代碼主要實現一個權限檢查攔截器,用于在請求進入控制器方法之前進行身份驗證。
作用范圍
操作對象
每天一個小知識,每天進步一點點?。?![加油][加油][加油]
喜歡這類文章,請關注、點贊、收藏、轉發,謝謝?。。?/p>
*請認真填寫需求信息,我們會在24小時內與您取得聯系。