之前是Mybatis XML配套的方式去寫,而MybaitsPlus是Mybatis的加強版,拋去了XML文件內容。後者雖然減小了不少繁瑣的SQL內容編寫,可是一樣的,對於複雜的SQL場景,相似流似的SQL生成仍是沒有XML寫法直觀。java
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.23</version> </dependency>
插件是方便自定義額外功能,好比分頁、模糊查詢處理特殊字符。咱們在編寫插件時,除了須要讓插件類實現 Interceptor 接口外,還須要經過註解標註 該插件的攔截點。所謂攔截點指的是插件所能攔截的方法,MyBatis 所容許攔截的方法以下:spring
5.1 原理sql
(1)配置加載插件數據庫
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration:加載任何實現org.apache.ibatis.plugin.Interceptor的自定義插件類。apache
public MybatisPlusAutoConfiguration(MybatisPlusProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider, ObjectProvider<List<MybatisPlusPropertiesCustomizer>> mybatisPlusPropertiesCustomizerProvider, ApplicationContext applicationContext) { this.properties = properties; this.interceptors = interceptorsProvider.getIfAvailable(); this.typeHandlers = typeHandlersProvider.getIfAvailable(); this.languageDrivers = languageDriversProvider.getIfAvailable(); this.resourceLoader = resourceLoader; this.databaseIdProvider = databaseIdProvider.getIfAvailable(); this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); this.mybatisPlusPropertiesCustomizers = mybatisPlusPropertiesCustomizerProvider.getIfAvailable(); this.applicationContext = applicationContext; }
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { // ... if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } // ... }
(2)Executor緩存
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSourcesession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // ... // 建立Executor final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) {...} finally {...} }
com.baomidou.mybatisplus.core.MybatisConfiguration#newExecutormybatis
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { if (useDeprecatedExecutor) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new MybatisBatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new MybatisReuseExecutor(this, transaction); } else { executor = new MybatisSimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new MybatisCachingExecutor(executor); } // 植入插件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } return super.newExecutor(transaction, executorType); }
org.apache.ibatis.plugin.InterceptorChain#pluginAllapp
public Object pluginAll(Object target) { // 遍歷攔截器 for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
案例插件 分佈式
public Object plugin(Object target) { return Plugin.wrap(target, this); }
org.apache.ibatis.plugin.Plugin#wrap
public static Object wrap(Object target, Interceptor interceptor) { // 獲取@Intercepts註解的信息,每個@Signature註解圈定一個方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 獲取目標類實現的接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // JDK動態代理,調用invoke方法 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }
org.apache.ibatis.plugin.Plugin#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { // 如何方法聲明的類包含@Signature聲明的方法,則調用攔截器 return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
(3)StatementHandler
com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doQuery
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 建立StatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog(), false); return stmt == null ? Collections.emptyList() : handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
org.apache.ibatis.session.Configuration#newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 植入插件 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
(4)ParameterHandler
org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // ... // 建立ParameterHandler this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); // 建立ResultSetHandler this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); }
org.apache.ibatis.session.Configuration#newParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 植入插件 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; }
(5)ResultHandler
org.apache.ibatis.session.Configuration#newResultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // 植入插件 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; }
4.2 實戰
(1)分頁插件
@Bean public PaginationInnerInterceptor paginationInnerInterceptor() { PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); return paginationInnerInterceptor; } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(ObjectProvider<List<InnerInterceptor>> innerInterceptorProviders) { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); List<InnerInterceptor> interceptors = innerInterceptorProviders.getIfAvailable(); if (!CollectionUtils.isEmpty(interceptors)) { for (InnerInterceptor innerInterceptor : interceptors) { mybatisPlusInterceptor.addInnerInterceptor(innerInterceptor); } } return mybatisPlusInterceptor; }
(2)模糊查詢處理特殊字符
@Bean public FuzzyQueryInterceptor fuzzyQueryInterceptor() { return new FuzzyQueryInterceptor(); }
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import priv.whh.std.boot.mybatis.plus.util.EscapeUtil; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.Properties; import java.util.Set; /** * 模糊查詢攔截器方法,處理模糊查詢中包含特殊字符(_、%、\) * */ @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 FuzzyQueryInterceptor implements Interceptor { private static final String LIKE = " like "; private static final String QUESTION_MARK = "?"; private static final String LIKE_WITH_QUESTION_MARK = " like ?"; private static final String UNDERLINE = "_"; private static final String PERCENT = "%"; private static final String EW = "ew"; private static final String EW_PARAM_NAME_VALUE_PAIRS = "ew.paramNameValuePairs."; private static final String DOUBLE_SLASH = "\\"; private static final String DOUBLE_SLASH_WITH_SPOT = "\\."; private static final String DOUBLE_SLASH_WITH_QUESTION_MARK = "\\?"; @Override public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException { // 攔截sql Object[] args = invocation.getArgs(); MappedStatement statement = (MappedStatement) args[0]; Object parameterObject = args[1]; BoundSql boundSql = statement.getBoundSql(parameterObject); String sql = boundSql.getSql(); // 處理特殊字符 Object param = modifyLikeSql(sql, parameterObject, boundSql); args[1] = param; // 返回 return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } @SuppressWarnings("unchecked") public static Object modifyLikeSql(String sql, Object parameterObject, BoundSql boundSql) { boolean paramJudge = parameterObject instanceof HashMap || parameterObject instanceof String; if (!paramJudge) { return parameterObject; } if (!sql.toLowerCase().contains(LIKE) || !sql.toLowerCase().contains(QUESTION_MARK)) { return parameterObject; } // 獲取關鍵字的個數(去重) String[] strList = sql.split(DOUBLE_SLASH_WITH_QUESTION_MARK); Set<String> keyNames = new HashSet<>(); for (int i = 0; i < strList.length; i++) { if (strList[i].toLowerCase().contains(LIKE)) { String keyName = boundSql.getParameterMappings().get(i).getProperty(); keyNames.add(keyName); } } // 對關鍵字進行特殊字符「清洗」,若是有特殊字符的,在特殊字符前添加轉義字符(\) for (String keyName : keyNames) { HashMap<String, Object> parameter; if (parameterObject instanceof HashMap) { parameter = (HashMap<String, Object>) parameterObject; } else { parameter = new HashMap<>(2); parameter.put(keyName, parameterObject); } if (keyName.contains(EW_PARAM_NAME_VALUE_PAIRS) && sql.toLowerCase().contains(LIKE_WITH_QUESTION_MARK)) { // 第一種狀況:在業務層進行條件構造產生的模糊查詢關鍵字 QueryWrapper<Object> wrapper = (QueryWrapper<Object>) parameter.get(EW); parameter = (HashMap<String, Object>) wrapper.getParamNameValuePairs(); String[] keyList = keyName.split(DOUBLE_SLASH_WITH_SPOT); // ew.paramNameValuePairs.MPGENVAL1,截取字符串以後,獲取第三個,即爲參數名 Object a = parameter.get(keyList[2]); boolean judge = a instanceof String && (a.toString().contains(UNDERLINE) || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT)); if (judge) { parameter.put(keyList[2], PERCENT + EscapeUtil.escapeChar(a.toString().substring(1, a.toString().length() - 1)) + PERCENT); } } else if (!keyName.contains(EW_PARAM_NAME_VALUE_PAIRS) && sql.toLowerCase().contains(LIKE_WITH_QUESTION_MARK)) { // 第二種狀況:未使用條件構造器,可是在service層進行了查詢關鍵字與模糊查詢符`%`手動拼接 Object a = parameter.get(keyName); boolean judge = a instanceof String && (a.toString().contains(UNDERLINE) || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT)); if (judge) { parameter.put(keyName, PERCENT + EscapeUtil.escapeChar(a.toString().substring(1, a.toString().length() - 1)) + PERCENT); } } else { // 第三種狀況:在Mapper類的註解SQL或者XML中進行了模糊查詢的拼接 Object a = parameter.get(keyName); boolean judge = a instanceof String && (a.toString().contains(UNDERLINE) || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT)); if (judge) { parameter.put(keyName, EscapeUtil.escapeChar(a.toString())); if (parameterObject instanceof String) { parameterObject = EscapeUtil.escapeChar(a.toString()); } } } } return parameterObject; } }
Q:Mybatis如何作到防SQL注入?
A: Mybatis 使用 #{?} 時,會進行sql的預編譯(select from t_user where a = ?);使用${?}時,不會進行sql的預編譯(select from t_user where a = 1),這樣的話,會有sql注入的風險。經測試,sql注入沒法注入兩條sql語句,會報錯。只能注入相似(select * from t_user where a = 1 or 1 = 1), 注入的變量爲 「‘1’ or 1 = 1」;
Q:除了配置的方式,QueryWrap能夠經過打斷點的方式查看組裝的SQL嗎?
A:不能
Q:數據庫時間戳接收用Date仍是LocalDateTime接收好?
Q:在繼承BaseMapper<User>文件中, MybatisPlus是否支持任意自定義對象返回,如返回Person?
Q:Mybatis-plus selectOne函數如何限制返回一條數據?
本文到這裏就結束了,感謝看到最後的朋友,都看到最後了,點個贊再走啊,若有不對之處還請多多指正。