從?PageHelper?到?MyBatis?Plugin執(zhí)行概要及實(shí)現(xiàn)原理
一、背景
在很多業(yè)務(wù)場(chǎng)景下我們需要去攔截 SQL
,達(dá)到不入侵原有代碼業(yè)務(wù)處理一些東西,比如:歷史記錄、分頁(yè)操作、數(shù)據(jù)權(quán)限過(guò)濾操作、SQL
執(zhí)行時(shí)間性能監(jiān)控等等,這里我們就可以用到 MyBatis
的插件 Plugin
。下面我們來(lái)了解一下 Plugin
到底是如何工作的。
使用過(guò) MyBatis
框架的朋友們肯定都聽(tīng)說(shuō)過(guò) PageHelper
這個(gè)分頁(yè)神器吧,其實(shí) PageHelper
的底層實(shí)現(xiàn)就是依靠 plugin
。下面我們來(lái)看一下 PageHelper
是如何利用 plugin
實(shí)現(xiàn)分頁(yè)的。
二、MyBatis 執(zhí)行概要圖
首先我們先看一下 MyBatis
的執(zhí)行流程圖,對(duì)其執(zhí)行流程有一個(gè)大體的認(rèn)識(shí)。
三、MyBatis 核心對(duì)象介紹
從 MyBatis
代碼實(shí)現(xiàn)的角度來(lái)看,MyBatis
的主要的核心部件有以下幾個(gè):
Configuration
:初始化基礎(chǔ)配置,比如MyBatis
的別名等,一些重要的類型對(duì)象,如,插件,映射器,ObjectFactory
和typeHandler
對(duì)象,MyBatis
所有的配置信息都維持在Configuration
對(duì)象之中。SqlSessionFactory
:SqlSession
工廠,用于生產(chǎn)SqlSession
。SqlSession
: 作為MyBatis
工作的主要頂層API
,表示和數(shù)據(jù)庫(kù)交互的會(huì)話,完成必要數(shù)據(jù)庫(kù)增刪改查功能Executor
:MyBatis
執(zhí)行器,是MyBatis
調(diào)度的核心,負(fù)責(zé)SQL
語(yǔ)句的生成和查詢緩存的維護(hù)StatementHandler
:封裝了JDBC Statement
操作,負(fù)責(zé)對(duì)JDBC Statement
的操作,如設(shè)置參數(shù)、將Statement
結(jié)果集轉(zhuǎn)換成List集合。ParameterHandler
:負(fù)責(zé)對(duì)用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement
所需要的參數(shù),ResultSetHandler
:負(fù)責(zé)將JDBC
返回的ResultSet
結(jié)果集對(duì)象轉(zhuǎn)換成List
類型的集合;TypeHandler
:負(fù)責(zé)java
數(shù)據(jù)類型和jdbc
數(shù)據(jù)類型之間的映射和轉(zhuǎn)換MappedStatement
:MappedStatement
維護(hù)了一條<select|update|delete|insert>
節(jié)點(diǎn)的封裝,SqlSource
:負(fù)責(zé)根據(jù)用戶傳遞的parameterObject
,動(dòng)態(tài)地生成SQL
語(yǔ)句,將信息封裝到BoundSql
對(duì)象中,并返回BoundSql
:表示動(dòng)態(tài)生成的SQL
語(yǔ)句以及相應(yīng)的參數(shù)信息
說(shuō)了這么多,怎么還沒(méi)進(jìn)入正題啊,別急,下面就開(kāi)始講解 Plugin
的實(shí)現(xiàn)原理。
四、Plugin 實(shí)現(xiàn)原理
MyBatis
支持對(duì) Executor、StatementHandler、PameterHandler和ResultSetHandler
接口進(jìn)行攔截,也就是說(shuō)會(huì)對(duì)這4種對(duì)象進(jìn)行代理。
下面我們結(jié)合 PageHelper
來(lái)講解 Plugin
是怎樣實(shí)現(xiàn)的。
1、定義 Plugin
要使用自定義 Plugin
首先要實(shí)現(xiàn) Interceptor
接口??梢酝ㄋ椎睦斫鉃橐粋€(gè) Plugin
就是一個(gè)攔截器。
public interface Interceptor { // 實(shí)現(xiàn)攔截邏輯 Object intercept(Invocation invocation) throws Throwable; // 獲取代理類 Object plugin(Object target); // 初始化配置 void setProperties(Properties properties); }
現(xiàn)在我們來(lái)看一下 PageHelper
是如何通過(guò) Plugin
實(shí)現(xiàn)分頁(yè)的。
@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}), } ) public class PageInterceptor implements Interceptor { //緩存count查詢的ms protected Cache<CacheKey, MappedStatement> msCountMap = null; private Dialect dialect; private String default_dialect_class = "com.github.pagehelper.PageHelper"; private Field additionalParametersField; @Override public Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; //由于邏輯關(guān)系,只會(huì)進(jìn)入一次 if(args.length == 4){ //4 個(gè)參數(shù)時(shí) boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 個(gè)參數(shù)時(shí) cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } List resultList; //調(diào)用方法判斷是否需要進(jìn)行分頁(yè),如果不需要,直接返回結(jié)果 if (!dialect.skip(ms, parameter, rowBounds)) { //反射獲取動(dòng)態(tài)參數(shù) Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql); //判斷是否需要進(jìn)行 count 查詢 if (dialect.beforeCount(ms, parameter, rowBounds)) { //創(chuàng)建 count 查詢的緩存 key CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql); countKey.update(MSUtils.COUNT); MappedStatement countMs = msCountMap.get(countKey); if (countMs == null) { //根據(jù)當(dāng)前的 ms 創(chuàng)建一個(gè)返回值為 Long 類型的 ms countMs = MSUtils.newCountMappedStatement(ms); msCountMap.put(countKey, countMs); } //調(diào)用方言獲取 count sql String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey); countKey.update(countSql); BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter); //當(dāng)使用動(dòng)態(tài) SQL 時(shí),可能會(huì)產(chǎn)生臨時(shí)的參數(shù),這些參數(shù)需要手動(dòng)設(shè)置到新的 BoundSql 中 for (String key : additionalParameters.keySet()) { countBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執(zhí)行 count 查詢 Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql); Long count = (Long) ((List) countResultList).get(0); //處理查詢總數(shù) //返回 true 時(shí)繼續(xù)分頁(yè)查詢,false 時(shí)直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //當(dāng)查詢總數(shù)為 0 時(shí),直接返回空的結(jié)果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } //判斷是否需要進(jìn)行分頁(yè)查詢 if (dialect.beforePage(ms, parameter, rowBounds)) { //生成分頁(yè)的緩存 key CacheKey pageKey = cacheKey; //處理參數(shù)對(duì)象 parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); //調(diào)用方言獲取分頁(yè) sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter); //設(shè)置動(dòng)態(tài)參數(shù) for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執(zhí)行分頁(yè)查詢 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不執(zhí)行分頁(yè)的情況下,也不執(zhí)行內(nèi)存分頁(yè) resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } } else { //rowBounds用參數(shù)值,不使用分頁(yè)插件處理時(shí),仍然支持默認(rèn)的內(nèi)存分頁(yè) resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { dialect.afterAll(); } } @Override public Object plugin(Object target) { //TODO Spring bean 方式配置時(shí),如果沒(méi)有配置屬性就不會(huì)執(zhí)行下面的 setProperties 方法,就不會(huì)初始化,因此考慮在這個(gè)方法中做一次判斷和初始化 //TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { //緩存 count ms msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties); String dialectClass = properties.getProperty("dialect"); if (StringUtil.isEmpty(dialectClass)) { dialectClass = default_dialect_class; } try { Class<?> aClass = Class.forName(dialectClass); dialect = (Dialect) aClass.newInstance(); } catch (Exception e) { throw new PageException(e); } dialect.setProperties(properties); try { //反射獲取 BoundSql 中的 additionalParameters 屬性 additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters"); additionalParametersField.setAccessible(true); } catch (NoSuchFieldException e) { throw new PageException(e); } } }
代碼太長(zhǎng)不看系列: 其實(shí)這段代碼最主要的邏輯就是在執(zhí)行 Executor
方法的時(shí)候,攔截 query
也就是查詢類型的 SQL
, 首先會(huì)判斷它是否需要分頁(yè),如果需要分頁(yè)就會(huì)根據(jù)查詢參數(shù)在 SQL
末尾加上 limit pageNum, pageSize
來(lái)實(shí)現(xiàn)分頁(yè)。
2、注冊(cè)攔截器
- 通過(guò)
SqlSessionFactoryBean
去構(gòu)建Configuration
添加攔截器并構(gòu)建獲取SqlSessionFactory
。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> { // ... 此處省略部分源碼 protected SqlSessionFactory buildSqlSessionFactory() throws IOException { // ... 此處省略部分源碼 // 查看是否注入攔截器,有的話添加到Interceptor集合里面 if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } } // ... 此處省略部分源碼 return this.sqlSessionFactoryBuilder.build(configuration); } // ... 此處省略部分源碼 }
- 通過(guò)原始的
XMLConfigBuilder
構(gòu)建configuration
添加攔截器
public class XMLConfigBuilder extends BaseBuilder { //解析配置 private void parseConfiguration(XNode root) { try { //省略部分代碼 pluginElement(root.evalNode("plugins")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); //調(diào)用InterceptorChain.addInterceptor configuration.addInterceptor(interceptorInstance); } } } }
上面是兩種不同的形式構(gòu)建 configuration
并添加攔截器 interceptor
,上面第二種一般是以前 XML
配置的情況,這里主要是解析配置文件的 plugin
節(jié)點(diǎn),根據(jù)配置的 interceptor
屬性實(shí)例化 Interceptor
對(duì)象,然后添加到 Configuration
對(duì)象中的 InterceptorChain
屬性中。
如果定義多個(gè)攔截器就會(huì)它們鏈起來(lái)形成一個(gè)攔截器鏈,初始化配置文件的時(shí)候就把所有的攔截器添加到攔截器鏈中。
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { //循環(huán)調(diào)用每個(gè)Interceptor.plugin方法 for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } // 添加攔截器 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
3、執(zhí)行攔截器
從以下代碼可以看出 MyBatis
在實(shí)例化 Executor、ParameterHandler、ResultSetHandler、StatementHandler
四大接口對(duì)象的時(shí)候調(diào)用 interceptorChain.pluginAll()
方法插入進(jìn)去的。
其實(shí)就是循環(huán)執(zhí)行攔截器鏈所有的攔截器的 plugin()
方法, MyBatis
官方推薦的 plugin
方法是 Plugin.wrap()
方法,這個(gè)就會(huì)生成代理類。
public class Configuration { protected final InterceptorChain interceptorChain = new InterceptorChain(); //創(chuàng)建參數(shù)處理器 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { //創(chuàng)建ParameterHandler ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); //插件在這里插入 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } //創(chuàng)建結(jié)果集處理器 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { //創(chuàng)建DefaultResultSetHandler ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); //插件在這里插入 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } //創(chuàng)建語(yǔ)句處理器 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //創(chuàng)建路由選擇語(yǔ)句處理器 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); //插件在這里插入 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction) { return newExecutor(transaction, defaultExecutorType); } //產(chǎn)生執(zhí)行器 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; //這句再做一下保護(hù),囧,防止粗心大意的人將defaultExecutorType設(shè)成null? executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //然后就是簡(jiǎn)單的3個(gè)分支,產(chǎn)生3種執(zhí)行器BatchExecutor/ReuseExecutor/SimpleExecutor if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } //如果要求緩存,生成另一種CachingExecutor(默認(rèn)就是有緩存),裝飾者模式,所以默認(rèn)都是返回CachingExecutor if (cacheEnabled) { executor = new CachingExecutor(executor); } //此處調(diào)用插件,通過(guò)插件可以改變Executor行為 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } }
4、Plugin 的動(dòng)態(tài)代理
我們首先看一下Plugin.wrap()
方法,這個(gè)方法的作用是為實(shí)現(xiàn)Interceptor注解的接口實(shí)現(xiàn)類生成代理對(duì)象的。
// 如果是Interceptor注解的接口的實(shí)現(xiàn)類會(huì)產(chǎn)生代理類 public static Object wrap(Object target, Interceptor interceptor) { //從攔截器的注解中獲取攔截的類名和方法信息 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); //取得要改變行為的類(ParameterHandler|ResultSetHandler|StatementHandler|Executor) Class<?> type = target.getClass(); //取得接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); //產(chǎn)生代理,是Interceptor注解的接口的實(shí)現(xiàn)類才會(huì)產(chǎn)生代理 if (interfaces.length > 0) { return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap)); } return target; }
Plugin
中的 getSignatureMap、 getAllInterfaces
兩個(gè)輔助方法,來(lái)幫助判斷是否為是否Interceptor注解的接口實(shí)現(xiàn)類。
//取得簽名Map,就是獲取Interceptor實(shí)現(xiàn)類上面的注解,要攔截的是那個(gè)類(Executor //,ParameterHandler, ResultSetHandler,StatementHandler)的那個(gè)方法 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { //取Intercepts注解 Intercepts interceptsAnnotation =interceptor.getClass().getAnnotation(Intercepts.class); //必須得有Intercepts注解,沒(méi)有報(bào)錯(cuò) if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } //value是數(shù)組型,Signature的數(shù)組 Signature[] sigs = interceptsAnnotation.value(); //每個(gè)class里有多個(gè)Method需要被攔截,所以這么定義 Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } //取得接口 private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { //攔截其他的無(wú)效 if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); } }
我們來(lái)看一下代理類的 query
方法,其實(shí)就是調(diào)用了 Plugin.invoke()
方法。代理類屏蔽了 intercept
方法的調(diào)用。
public final List query(MappedStatement mappedStatement, Object object, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException { try { // 這里的 h 就是一個(gè) Plugin return (List)this.h.invoke(this, m5, new Object[]{mappedStatement, object, rowBounds, resultHandler, cacheKey, boundSql}); } catch (Error | RuntimeException | SQLException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } }
最后 Plugin.invoke()
就是判斷當(dāng)前方法是否攔截,如果需要攔截則會(huì)調(diào)用 Interceptor.intercept()
對(duì)當(dāng)前方法執(zhí)行攔截邏輯。
public class Plugin implements InvocationHandler { ... @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //獲取需要攔截的方法 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); //是Interceptor實(shí)現(xiàn)類注解的方法才會(huì)攔截處理 if (methods != null && methods.contains(method)) { //調(diào)用Interceptor.intercept,即調(diào)用自己寫的邏輯 return interceptor.intercept(new Invocation(target, method, args)); } //最后執(zhí)行原來(lái)邏輯 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } ...
總結(jié)
我們以 PageHelper
為切入點(diǎn)講解了 MyBatis Plugin
的實(shí)現(xiàn)原理,其中 MyBatis 攔截器用到責(zé)任鏈模式+動(dòng)態(tài)代理+反射機(jī)制。 通過(guò)上面的分析可以知道,所有可能被攔截的處理類都會(huì)生成一個(gè)代理類,如果有 N 個(gè)攔截器,就會(huì)有 N 個(gè)代理,層層生成動(dòng)態(tài)代理是比較耗性能的。而且雖然能指定插件攔截的位置,但這個(gè)是在執(zhí)行方法時(shí)利用反射動(dòng)態(tài)判斷的,初始化的時(shí)候就是簡(jiǎn)單的把攔截器插入到了所有可以攔截的地方。所以盡量不要編寫不必要的攔截器,并且攔截器盡量不要寫復(fù)雜的邏輯。
以上就是從 PageHelper 到 MyBatis Plugin執(zhí)行概要及實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于PageHelper MyBatis Plugin的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中finally和return的關(guān)系實(shí)例解析
這篇文章主要介紹了Java中finally和return的關(guān)系實(shí)例解析,總結(jié)了二者的關(guān)系,然后分享了相關(guān)代碼示例,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02java線程并發(fā)blockingqueue類使用示例
BlockingQueue是一種特殊的Queue,若BlockingQueue是空的,從BlockingQueue取東西的操作將會(huì)被阻斷進(jìn)入等待狀態(tài)直到BlocingkQueue進(jìn)了新貨才會(huì)被喚醒,下面是用BlockingQueue來(lái)實(shí)現(xiàn)Producer和Consumer的例子2014-01-01java生成申請(qǐng)單序列號(hào)的實(shí)現(xiàn)方法
申請(qǐng)單序列號(hào)一般要求根據(jù)一定的規(guī)則生成后幾位連續(xù)的字符串,下面是我項(xiàng)目中使用的生成序列號(hào)的代碼,其中用到了鎖機(jī)制,有需要的朋友可以參考一下2014-01-01Java 客戶端向服務(wù)端上傳mp3文件數(shù)據(jù)的實(shí)例代碼
這篇文章主要介紹了Java 客戶端向服務(wù)端上傳mp3文件數(shù)據(jù)的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09Java中redisTemplate注入失敗NullPointerException異常問(wèn)題解決
這篇文章主要介紹了Java中redisTemplate注入失敗NullPointerException異常問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2023-08-08詳解Java如何實(shí)現(xiàn)加密或者解密PDF文檔
PDF文檔加密是一種用于保護(hù)文件內(nèi)容的功能。這篇文章主要介紹了Java實(shí)現(xiàn)加密或者解密PDF文檔的方法,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-03-03Mybatis之動(dòng)態(tài)SQL使用小結(jié)(全網(wǎng)最新)
MyBatis令人喜歡的一大特性就是動(dòng)態(tài)SQL,?在使用JDBC的過(guò)程中,?根據(jù)條件進(jìn)行SQL的拼接是很麻煩且很容易出錯(cuò)的,MyBatis通過(guò)OGNL來(lái)進(jìn)行動(dòng)態(tài)SQL的使用解決了這個(gè)麻煩,對(duì)Mybatis動(dòng)態(tài)SQL相關(guān)知識(shí)感興趣的朋友跟隨小編一起看看吧2024-05-05