Mybatis攔截器實現(xiàn)自定義需求
前言
最近在參加金石計劃,在考慮寫什么的時,想到自己在項目中使用過的mybatis的插件,就想趁這個機會聊一聊我們接觸頻繁的Mybatis.
如果是使用過Mybatis的小伙伴,那么我們接觸過的第一個Mybatis的插件自然就是分頁插件(Mybatis-PageHelper)啦。
你有了解過它是如何實現(xiàn)的嗎?你有沒有自己編寫 Mybatis 插件去實現(xiàn)一些自定義需求呢?
插件是一種常見的擴展方式,大多數(shù)開源框架也都支持用戶通過添加自定義插件的方式來擴展或改變框架原有的功能。
Mybatis 中也提供了插件的功能,雖然叫插件,但是實際上是通過攔截器( Interceptor )實現(xiàn)的,通過攔截某些方法的調(diào)用,在執(zhí)行目標邏輯之前插入我們自己的邏輯實現(xiàn)。另外在 MyBatis 的插件模塊中還涉及責任鏈模式和 JDK 動態(tài)代理~
文章大綱:
一、應用場景
- 一些字段的自動填充
- SQL語句監(jiān)控、打印、數(shù)據(jù)權(quán)限等
- 數(shù)據(jù)加解密操作、數(shù)據(jù)脫敏操作
- 分頁插件
- 參數(shù)、結(jié)果集的類型轉(zhuǎn)換
這些都是一些可以使用Mybatis插件實現(xiàn)的場景,當然也可以使用其他的方式來實現(xiàn),只不過攔截的地方不一樣罷了,有早有晚。
二、Mybatis實現(xiàn)自定義攔截器
我們用自定義攔截器實現(xiàn)一個相對簡單的需求,在大多數(shù)表設(shè)計中,都會有create_time和update_time
等字段,在創(chuàng)建或更新時需要更新相關(guān)字段。
如果是使用過MybatisPlus
的小伙伴,可能知道在MybatisPlus
中有一個自動填充功能,通過實現(xiàn)MetaObjectHandler
接口中的方法來進行實現(xiàn)(主要的實現(xiàn)代碼在com.baomidou.mybatisplus.core.MybatisParameterHandler
中).
但使用Mybatis
,并沒有相關(guān)的方法或 API 可以直接來實現(xiàn)。所以我們這次就用以此處作為切入點,自定義攔截器來實現(xiàn)類似的自動填充功能。
編寫步驟
- 編寫一個攔截器類實現(xiàn) Interceptor 接口
- 添加攔截注解 @Intercepts
- 在xml文件中配置攔截器或者添加到Configuration中
基礎(chǔ)的環(huán)境我就不再貼出來啦哈,直接上三個步驟的代碼
2.1、編寫攔截器
package com.nzc.interceptor; ? import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.springframework.beans.factory.annotation.Value; ? import java.lang.reflect.Field; import java.util.*; ? /** * @author 寧在春 * @version 1.0 * @description: 通過實現(xiàn)攔截器來實現(xiàn)部分字段的自動填充功能 * @date 2023/4/6 21:49 */ @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Slf4j public class MybatisMetaInterceptor implements Interceptor { ? @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; String sqlId = mappedStatement.getId(); log.info("------sqlId------" + sqlId); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); Object parameter = invocation.getArgs()[1]; log.info("------sqlCommandType------" + sqlCommandType); log.info("攔截查詢請求 Executor#update 方法" + invocation.getMethod()); if (parameter == null) { return invocation.proceed(); } if (SqlCommandType.INSERT == sqlCommandType) { ? Field[] fields = getAllFields(parameter); for (Field field : fields) { log.info("------field.name------" + field.getName()); try { // 注入創(chuàng)建時間 if ("createTime".equals(field.getName())) { field.setAccessible(true); Object local_createDate = field.get(parameter); field.setAccessible(false); if (local_createDate == null || local_createDate.equals("")) { field.setAccessible(true); field.set(parameter, new Date()); field.setAccessible(false); } } } catch (Exception e) { } } } if (SqlCommandType.UPDATE == sqlCommandType) { Field[] fields = getAllFields(parameter); for (Field field : fields) { log.info("------field.name------" + field.getName()); try { if ("updateTime".equals(field.getName())) { field.setAccessible(true); field.set(parameter, new Date()); field.setAccessible(false); } } catch (Exception e) { e.printStackTrace(); } } } return invocation.proceed(); } ? @Override public Object plugin(Object target) { return Interceptor.super.plugin(target); } // 稍后會展開說的 @Override public void setProperties(Properties properties) { System.out.println("=======begin"); System.out.println(properties.getProperty("param1")); System.out.println(properties.getProperty("param2")); Interceptor.super.setProperties(properties); System.out.println("=======end"); } ? /** * 獲取類的所有屬性,包括父類 * * @param object * @return */ public static Field[] getAllFields(Object object) { Class<?> clazz = object.getClass(); List<Field> fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields = new Field[fieldList.size()]; fieldList.toArray(fields); return fields; } } ?
2.2、添加到Mybatis配置
我這里使用的JavaConfig的方式
package com.nzc.config; ? import com.nzc.interceptor.*; import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; ? @Configuration public class MyBatisConfig { ? @Bean public ConfigurationCustomizer configurationCustomizer() { return new ConfigurationCustomizer() { @Override public void customize(org.apache.ibatis.session.Configuration configuration) { // 開啟駝峰命名映射 configuration.setMapUnderscoreToCamelCase(true); MybatisMetaInterceptor mybatisMetaInterceptor = new MybatisMetaInterceptor(); Properties properties = new Properties(); properties.setProperty("param1","javaconfig-value1"); properties.setProperty("param2","javaconfig-value2"); mybatisMetaInterceptor.setProperties(properties); configuration.addInterceptor(mybatisMetaInterceptor); } }; } }
如果是xml配置的話,則是如下: property 是設(shè)置 攔截器中需要用到的參數(shù)
<configuration> <plugins> <plugin interceptor="com.nzc.interceptor.MybatisMetaInterceptor"> <property name="param1" value="value1"/> <property name="param2" value="value2"/> </plugin> </plugins> </configuration>
2.3、測試
測試代碼:實現(xiàn)了一個SysMapper的增刪改查
package com.nzc.mapper; ?? import com.nzc.entity.SysUser; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; ? import java.util.List; ? /** * @author 寧在春 * @description 針對表【sys_user】的數(shù)據(jù)庫操作Mapper */ @Mapper public interface SysUserMapper { ? @Select("SELECT * FROM tb_sys_user") List<SysUser> list(); ? @Insert("insert into tb_sys_user(id,username,realname,create_time,update_time) values (#{id}, #{username}, #{realname}, #{createTime}, #{updateTime})") Boolean insert(SysUser sysUser); ? @Update("update tb_sys_user set username=#{username} , realname=#{realname},update_time=#{updateTime} where id=#{id}") boolean update(SysUser sysUser); }
/** * @author 寧在春 * @version 1.0 * @description: TODO * @date 2023/4/6 21:38 */ @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class SysUserMapperTest { ? @Autowired private SysUserMapper sysUserMapper; ? ? @Test public void test1(){ System.out.println(sysUserMapper.list()); } ? @Test public void test2(){ SysUser sysUser = new SysUser(); sysUser.setId("1235"); sysUser.setUsername("nzc5"); sysUser.setRealname("nzc5"); System.out.println(sysUserMapper.insert(sysUser)); } ? @Test public void test3(){ SysUser sysUser = new SysUser(); sysUser.setId("1235"); sysUser.setUsername("nzc7"); sysUser.setRealname("nzc5"); System.out.println(sysUserMapper.update(sysUser)); } } ?
當然重點不在這里,而是在我們打印的日志上,一起來看看效果吧
此處相關(guān)日志對應Interceptor中的日志打印,想要了解的更為詳細的可以debug查看一番。
2.4、小結(jié)
通過這個小小的案例,我想大伙對于Mybatis中的攔截器應當是沒有那般陌生了吧,接下來再來仔細聊聊吧
如果你使用過
MybatisPlus
的話,在讀完這篇博文后,可以思考思考下面這個問題,或去看一看源碼,將知識串聯(lián)起來,如果可以的話,記得把答案貼到評論區(qū)啦~~~思考:還記得這一小節(jié)開始我們聊到的
MybatisPlus
實現(xiàn)的自動填充功能嗎?它是怎么實現(xiàn)的呢?
三、攔截器接口介紹
MyBatis 插件可以用來實現(xiàn)攔截器接口 Interceptor ,在實現(xiàn)類中對攔截對象和方法進行處理
public interface Interceptor { // 執(zhí)行攔截邏輯的方法 Object intercept(Invocation invocation) throws Throwable; ? //這個方法的參數(shù) target 就是攔截器要攔截的對象,該方法會在創(chuàng)建被攔截的接口實現(xiàn)類時被調(diào)用。 //該方法的實現(xiàn)很簡單 ,只需要調(diào)用 MyBatis 提供的 Plug 類的 wrap 靜態(tài)方法就可以通過 Java 動態(tài)代理攔截目標對象。 default Object plugin(Object target) { return Plugin.wrap(target, this); } ? //這個方法用來傳遞插件的參數(shù),可以通過參數(shù)來改變插件的行為 default void setProperties(Properties properties) { // NOP } }
有點懵沒啥事,一個一個展開說:
intercept 方法
Object intercept(Invocation invocation) throws Throwable;
簡單說就是執(zhí)行攔截邏輯的方法
,但不得不說這句話是個高度概括~
首先我們要明白參數(shù)Invocation
是個什么東東:
public class Invocation { ? private final Object target; // 攔截的對象信息 private final Method method; // 攔截的方法信息 private final Object[] args; // 攔截的對象方法中的參數(shù) ? public Invocation(Object target, Method method, Object[] args) { this.target = target; this.method = method; this.args = args; } ? // get... // 利用反射來執(zhí)行攔截對象的方法 public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } }
聯(lián)系我們之前實現(xiàn)的自定義攔截器上的注解:
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
target
對應我們攔截的Executor
對象method
對應Executor#update
方法args
對應Executor#update#args
參數(shù)
plugin方法
這個方法其實也很好說:
那就是Mybatis在創(chuàng)建攔截器代理時候會判斷一次,當前這個類 Interceptor 到底需不需要生成一個代理進行攔截,如果需要攔截,就生成一個代理對象,這個代理就是一個 {@link Plugin},它實現(xiàn)了jdk的動態(tài)代理接口 {@link InvocationHandler},如果不需要代理,則直接返回目標對象本身 加載時機:該方法在 mybatis 加載核心配置文件時被調(diào)用
default Object plugin(Object target) { return Plugin.wrap(target, this); }
public class Plugin implements InvocationHandler { ? ? // 利用反射,獲取這個攔截器 MyInterceptor 的注解 Intercepts和Signature,然后解析里面的值, // 1 先是判斷要攔截的對象是哪一個 // 2 然后根據(jù)方法名稱和參數(shù)判斷要對哪一個方法進行攔截 // 3 根據(jù)結(jié)果做出決定,是返回一個對象呢還是代理對象 public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 這邊就是判斷當前的interceptor是否包含在 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } //如果不需要代理,則直接返回目標對象本身 return target; } ? //.... ? }
setProperties方法
在攔截器中可能需要使用到一些變量參數(shù),并且這個參數(shù)是可配置的,這個時候我們就可以使用這個方法啦,加載時機:該方法在 mybatis 加載核心配置文件時被調(diào)用
default void setProperties(Properties properties) { // NOP }
關(guān)于如何使用:
javaConfig方式設(shè)置:
@Bean public ConfigurationCustomizer configurationCustomizer() { return new ConfigurationCustomizer() { @Override public void customize(org.apache.ibatis.session.Configuration configuration) { // 開啟駝峰命名映射 configuration.setMapUnderscoreToCamelCase(true); MybatisMetaInterceptor mybatisMetaInterceptor = new MybatisMetaInterceptor(); Properties properties = new Properties(); properties.setProperty("param1","javaconfig-value1"); properties.setProperty("param2","javaconfig-value2"); mybatisMetaInterceptor.setProperties(properties); configuration.addInterceptor(mybatisMetaInterceptor); } }; }
通過mybatis-config.xml
文件進行配置
<configuration> <plugins> <plugin interceptor="com.nzc.interceptor.MybatisMetaInterceptor"> <property name="param1" value="value1"/> <property name="param2" value="value2"/> </plugin> </plugins> </configuration>
測試效果就是測試案例上那般,通過了解攔截器接口的信息,對于之前的案例不再是那般模糊啦
接下來再接著聊一聊攔截器上面那一坨注解信息是用來干嘛的吧,
注意
當配置多個攔截器時, MyBatis 會遍歷所有攔截器,按順序執(zhí)行攔截器的 plugin 口方法, 被攔截的對象就會被層層代理。
在執(zhí)行攔截對象的方法時,會一層層地調(diào)用攔截器,攔截器通 invocation proceed()調(diào)用下層的方法,直到真正的方法被執(zhí)行。
方法執(zhí)行的結(jié)果 從最里面開始向外 層層返回,所以如果存在按順序配置的三個簽名相同的攔截器, MyBaits 會按照 C>B>A>target.proceed()>A>B>C 的順序執(zhí)行。如果簽名不同, 就會按照 MyBatis 攔截對象的邏輯執(zhí)行.
這也是我們最開始談到的Mybatis插件模塊所使用的設(shè)計模式-責任鏈模式。
四、攔截器注解介紹
上一個章節(jié),我們只說明如何實現(xiàn)Interceptor
接口來實現(xiàn)攔截,卻沒有說明要攔截的對象是誰,在什么時候進行攔截.就關(guān)系到我們之前編寫的注解信息啦.
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
這兩個注解用來配置攔截器要攔截的接口的方法。
@Intercepts({})
注解中是一個@Signature()
數(shù)組,可以在一個攔截器中同時攔截不同的接口和方法。
MyBatis 允許在己映射語句執(zhí)行過程中的某一點進行攔截調(diào)用。默認情況下, MyBatis 允許使用插件來攔截的接口包括以下幾個。
- Executor
- ParameterHandler
- ResultSetHandler
- StatementHandler
@Signature
注解包含以下三個屬性。
- type 設(shè)置攔截接口,可選值是前面提到的4個接口
- method 設(shè)置攔截接口中的方法名 可選值是前面4個接口中所對應的方法,需要和接口匹配
- args 設(shè)置攔截方法的參數(shù)類型數(shù)組 通過方法名和參數(shù)類型可以確定唯一一個方法
Executor 接口
下面就是Executor接口的類信息
public interface Executor { ? int update(MappedStatement ms, Object parameter) throws SQLException; ? <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; ? <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; ? <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; ? List<BatchResult> flushStatements() throws SQLException; ? void commit(boolean required) throws SQLException; ? void rollback(boolean required) throws SQLException; ? CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql); ? boolean isCached(MappedStatement ms, CacheKey key); ? void clearLocalCache(); ? void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType); ? Transaction getTransaction(); ? void close(boolean forceRollback); ? boolean isClosed(); ? void setExecutorWrapper(Executor executor); ? }
我只會簡單說一些最常用的~
1、update
int update(MappedStatement ms, Object parameter) throws SQLException;
該方法會在所有的 INSERT、UPDATE、DELETE 執(zhí)行時被調(diào)用,因此如果想要攔截這類操作,可以攔截該方法。接口方法對應的簽名如下。
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
2、query
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; ? <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
該方法會在所有 SELECT 查詢方法執(zhí)行時被調(diào)用 通過這個接口參數(shù)可以獲取很多有用的信息,這也是最常被攔截的方法。
@Intercepts({@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} )})
3、queryCursor:
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
該方法只有在查詢 的返回值類型為 Cursor 時被調(diào)用 。接口方法對應的簽名類似于之前的。
//該方法只在通過 SqlSession 方法調(diào)用 commit 方法時才被調(diào)用 void commit(boolean required) throws SQLException; //該方法只在通過 SqlSessio口方法調(diào)用 rollback 方法時才被調(diào)用 void rollback(boolean required) throws SQLException; //該方法只在通過 SqlSession 方法獲取數(shù)據(jù)庫連接時才被調(diào)用, Transaction getTransaction(); //該方法只在延遲加載獲取新的 Executor 后才會被執(zhí)行 void close(boolean forceRollback); //該方法只在延遲加載執(zhí)行查詢方法前被執(zhí)行 boolean isClosed();
注解的編寫方法都是類似的。
ParameterHandler 接口
public interface ParameterHandler { ? //該方法只在執(zhí)行存儲過程處理出參的時候被調(diào)用 Object getParameterObject(); //該方法在所有數(shù)據(jù)庫方法設(shè)置 SQL 參數(shù)時被調(diào)用。 void setParameters(PreparedStatement ps) throws SQLException; }
我都寫一塊啦,如果要攔截某一個的話只寫一個即可
@Intercepts({ @Signature(type = ParameterHandler.class, method = "getParameterObject", args = {}), @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}) })
ResultSetHandler 接口
public interface ResultSetHandler { //該方法會在除存儲過程及返回值類型為 Cursor 以外的查詢方法中被調(diào)用。 <E> List<E> handleResultSets(Statement stmt) throws SQLException; //只會在返回值類型為 ursor 查詢方法中被調(diào)用 <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException; //只在使用存儲過程處理出參時被調(diào)用 , void handleOutputParameters(CallableStatement cs) throws SQLException; }
@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}), @Signature(type = ResultSetHandler.class, method = "handleCursorResultSets", args = {Statement.class}), @Signature(type = ResultSetHandler.class, method = "handleOutputParameters", args = {CallableStatement.class}) })
StatementHandler 接口
public interface StatementHandler { //該方法會在數(shù)據(jù)庫執(zhí)行前被調(diào)用 優(yōu)先于當前接口中的其他方法而被執(zhí)行 Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; //該方法在 prepare 方法之后執(zhí)行,用于處理參數(shù)信息 void parameterize(Statement statement) throws SQLException; //在全局設(shè)置配置 defaultExecutorType BATCH 時,執(zhí)行數(shù)據(jù)操作才會調(diào)用該方法 void batch(Statement statement) throws SQLException; //執(zhí)行UPDATE、DELETE、INSERT方法時執(zhí)行 int update(Statement statement) throws SQLException; //執(zhí)行 SELECT 方法時調(diào)用,接口方法對應的簽名如下。 <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; ? <E> Cursor<E> queryCursor(Statement statement) throws SQLException; ? //獲取實際的SQL字符串 BoundSql getBoundSql(); ? ParameterHandler getParameterHandler(); ? }
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class}), @Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "query", args = {Statement.class,ResultHandler.class}), @Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "getBoundSql", args = {}), @Signature(type = StatementHandler.class, method = "getParameterHandler", args = {}) }
如果有時間的話,我會更加建議看了的小伙伴,自己去實現(xiàn)接口做個測試,驗證一番,也能了解的更徹底些??磿?,很多時候知識的記憶還是淺的。
五、進一步思考
看完這篇文章后,不知道你有沒有什么收獲。
再次看看這張文章大綱的圖吧
試著思考思考下面幾個問題:
Mybatis插件適用于哪些場景?回憶一下你做過的項目,是否有可以使用Mybatis插件來實現(xiàn)的呢?你可以編寫一個Mybatis插件了嗎?感興趣的話,你可以試著去了解一下Mybatis分頁插件的實現(xiàn)方式。
最后留下一個遇到的問題,也是下一篇文章可能會寫的吧,同時也使用到了今天所談到了的攔截器。
在項目中,你們都是如何針對表中某些字段進行加解密的呢?
到此這篇關(guān)于Mybatis攔截器實現(xiàn)自定義需求的文章就介紹到這了,更多相關(guān)Mybatis自定義攔截器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一篇文章告訴你JAVA Mybatis框架的核心原理到底有多重要
yBatis的底層操作封裝了JDBC的API,MyBatis的工作原理以及核心流程與JDBC的使用步驟一脈相承,MyBatis的核心對象(SqlSession,Executor)與JDBC的核心對象(Connection,Statement)相互對應2021-06-06Java?@Scheduled定時任務不執(zhí)行解決辦法
這篇文章主要給大家介紹了關(guān)于Java?@Scheduled定時任務不執(zhí)行解決的相關(guān)資料,當@Scheduled定時任務不執(zhí)行時可以根據(jù)以下步驟進行排查和解決,需要的朋友可以參考下2023-10-10Java中關(guān)于char類型變量能夠輸出中文的問題
這篇文章主要介紹了Java中關(guān)于char類型變量能夠輸出中文的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12Java中防止數(shù)據(jù)重復提交超簡單的6種方法
在平時開發(fā)中,如果網(wǎng)速比較慢的情況下,用戶提交表單后,發(fā)現(xiàn)服務器半天都沒有響應,那么用戶可能會以為是自己沒有提交表單,就會再點擊提交按鈕重復提交表單,這篇文章主要給大家介紹了關(guān)于Java中防止數(shù)據(jù)重復提交超簡單的6種方法,需要的朋友可以參考下2021-11-11SpringBoot Shiro授權(quán)實現(xiàn)過程解析
這篇文章主要介紹了SpringBoot Shiro授權(quán)實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-11-11