Mybatis #foreach中相同的變量名導(dǎo)致值覆蓋的問題解決
背景
使用Mybatis中執(zhí)行如下查詢:
單元測(cè)試
@Test
public void test1() {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
CommonMapper mapper = sqlSession.getMapper(CommonMapper.class);
QueryCondition queryCondition = new QueryCondition();
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
queryCondition.setWidthList(list);
System.out.println(mapper.findByCondition(queryCondition));
}
}
XML
<select id="findByCondition" parameterType="cn.liupjie.pojo.QueryCondition" resultType="cn.liupjie.pojo.Test">
select * from test
<where>
<if test="id != null">
and id = #{id,jdbcType=INTEGER}
</if>
<if test="widthList != null and widthList.size > 0">
<foreach collection="widthList" open="and width in (" close=")" item="width" separator=",">
#{width,jdbcType=INTEGER}
</foreach>
</if>
<if test="width != null">
and width = #{width,jdbcType=INTEGER}
</if>
</where>
</select>
打印的SQL:
DEBUG [main] - ==> Preparing: select * from test WHERE width in ( ? , ? , ? ) and width = ?
DEBUG [main] - ==> Parameters: 1(Integer), 2(Integer), 3(Integer), 3(Integer)
Mybatis版本
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
這是公司的老項(xiàng)目,在迭代的過程中遇到了此問題,以此記錄!
PS: 此bug在mybatis-3.4.5版本中已經(jīng)解決。并且Mybatis維護(hù)者也建議不要在item/index中使用重復(fù)的變量名。


問題原因(簡(jiǎn)略版)
- 在獲取到DefaultSqlSession之后,會(huì)獲取到Mapper接口的代理類,通過調(diào)用代理類的方法來執(zhí)行查詢
- 真正執(zhí)行數(shù)據(jù)庫(kù)查詢之前,需要將可執(zhí)行的SQL拼接好,此操作在DynamicSqlSource#getBoundSql方法中執(zhí)行
- 當(dāng)解析到foreach標(biāo)簽時(shí),每次循環(huán)都會(huì)緩存一個(gè)item屬性值與變量值之間的映射(如:width:1),當(dāng)foreach標(biāo)簽解析完成后,緩存的參數(shù)映射關(guān)系中就保留了一個(gè)(width:3)
- 當(dāng)解析到最后一個(gè)if標(biāo)簽時(shí),由于width變量有值,因此if判斷為true,正常執(zhí)行拼接,導(dǎo)致出錯(cuò)
- 3.4.5版本中,在foreach標(biāo)簽解析完成后,增加了兩行代碼來解決這個(gè)問題。
//foreach標(biāo)簽解析完成后,從bindings中移除item context.getBindings().remove(item); context.getBindings().remove(index);
Mybatis流程源碼解析(長(zhǎng)文警告,按需自取)
一、獲取SqlSessionFactory
入口,跟著build方法走
//獲取SqlSessionFactory, 解析完成后,將XML中的內(nèi)容封裝到一個(gè)Configuration對(duì)象中, //使用此對(duì)象構(gòu)造一個(gè)DefaultSqlSessionFactory對(duì)象,并返回 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
來到SqlSessionFactoryBuilder#build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//獲取XMLConfigBuilder,在XMLConfigBuilder的構(gòu)造方法中,會(huì)創(chuàng)建XPathParser對(duì)象
//在創(chuàng)建XPathParser對(duì)象時(shí),會(huì)將mybatis-config.xml文件轉(zhuǎn)換成Document對(duì)象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//調(diào)用XMLConfigBuilder#parse方法開始解析Mybatis的配置文件
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
跟著parse方法走,來到XMLConfigBuilder#parseConfiguration方法
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//這里解析mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
來到mapperElement方法
//本次mappers配置:<mapper resource="xml/CommomMapper.xml"/>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//因此走這里,讀取xml文件,并開始解析
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//這里同上文創(chuàng)建XMLConfigBuilder對(duì)象一樣,在內(nèi)部構(gòu)造時(shí),也將xml文件轉(zhuǎn)換為了一個(gè)Document對(duì)象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
XMLMapperBuilder類,負(fù)責(zé)解析SQL語(yǔ)句所在XML中的內(nèi)容
//parse方法
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//解析mapper標(biāo)簽
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
//configurationElement方法
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
//解析各種類型的SQL語(yǔ)句:select|insert|update|delete
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//創(chuàng)建XMLStatementBuilder對(duì)象
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//解析
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
XMLStatementBuilder負(fù)責(zé)解析單個(gè)select|insert|update|delete節(jié)點(diǎn)
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//判斷databaseId是否匹配,將namespace+'.'+id拼接,判斷是否已經(jīng)存在此id
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
//獲取參數(shù)類型
String parameterType = context.getStringAttribute("parameterType");
//獲取參數(shù)類型的class對(duì)象
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//獲取resultType的class對(duì)象
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//獲取select|insert|update|delete類型
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//獲取SqlSource對(duì)象,langDriver為默認(rèn)的XMLLanguageDriver,在new Configuration時(shí)設(shè)置
//若sql中包含元素節(jié)點(diǎn)或$,則返回DynamicSqlSource,否則返回RawSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
二、獲取SqlSession
由上文可知,此處的SqlSessionFactory使用的是DefaultSqlSessionFactory
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//創(chuàng)建執(zhí)行器,默認(rèn)是SimpleExecutor
//如果在配置文件中開啟了緩存(默認(rèn)開啟),則是CachingExecutor
final Executor executor = configuration.newExecutor(tx, execType);
//返回DefaultSqlSession對(duì)象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
這里獲取到了一個(gè)DefaultSqlSession對(duì)象
三、執(zhí)行SQL
獲取CommonMapper的對(duì)象,這里CommonMapper是一個(gè)接口,因此是一個(gè)代理對(duì)象,代理類是MapperProxy
org.apache.ibatis.binding.MapperProxy@72cde7cc
執(zhí)行Query方法,來到MapperProxy的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//緩存
final MapperMethod mapperMethod = cachedMapperMethod(method);
//執(zhí)行操作:select|insert|update|delete
return mapperMethod.execute(sqlSession, args);
}
執(zhí)行操作時(shí),根據(jù)SELECT操作,以及返回值類型(反射方法獲?。┐_定executeForMany方法
caseSELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
來到executeForMany方法中,就可以看到執(zhí)行查詢的操作,由于這里沒有進(jìn)行分頁(yè)查詢,因此走else
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
來到DefaultSqlSession#selectList方法中
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根據(jù)key(namespace+"."+id)來獲取MappedStatement對(duì)象
//MappedStatement對(duì)象中封裝了解析好的SQL信息
MappedStatement ms = configuration.getMappedStatement(statement);
//通過CachingExecutor#query執(zhí)行查詢
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//解析SQL為可執(zhí)行的SQL
BoundSql boundSql = ms.getBoundSql(parameter);
//獲取緩存的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//執(zhí)行查詢
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
MappedStatement#getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
//解析SQL
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
//檢查是否有嵌套的ResultMap
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
由上文,此次語(yǔ)句由于SQL中包含元素節(jié)點(diǎn),因此是DynamicSqlSource。由此來到DynamicSqlSource#getBoundSql。
rootSqlNode.apply(context);這段代碼便是在執(zhí)行SQL解析。
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//執(zhí)行SQL解析
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
打上斷點(diǎn),跟著解析流程,來到解析foreach標(biāo)簽的代碼,F(xiàn)orEachSqlNode#apply
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
//解析open屬性
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first) {
context = new PrefixedContext(context, "");
} else if (separator != null) {
context = new PrefixedContext(context, separator);
} else {
context = new PrefixedContext(context, "");
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
//集合中的元素是Integer,走else
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
//使用index屬性
applyIndex(context, i, uniqueNumber);
//使用item屬性
applyItem(context, o, uniqueNumber);
}
//當(dāng)foreach中使用#號(hào)時(shí),會(huì)將變量替換為占位符(類似__frch_width_0)(StaticTextSqlNode)
//當(dāng)使用$符號(hào)時(shí),會(huì)將值直接拼接到SQL中(TextSqlNode)
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
return true;
}
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
//在參數(shù)映射中綁定item屬性值與集合值的關(guān)系
//第一次:(width:1)
//第二次:(width:2)
//第三次:(width:3)
context.bind(item, o);
//在參數(shù)映射中綁定處理后的item屬性值與集合值的關(guān)系
//第一次:(__frch_width_0:1)
//第二次:(__frch_width_1:2)
//第三次:(__frch_width_2:3)
context.bind(itemizeItem(item, i), o);
}
}
到這里,結(jié)果就清晰了,在解析foreach標(biāo)簽時(shí),每次循環(huán)都會(huì)將item屬性值與參數(shù)集合中的值進(jìn)行綁定,到最后就會(huì)保留(width:3)的映射關(guān)系,而在解析完foreach標(biāo)簽后,會(huì)解析最后一個(gè)if標(biāo)簽,此時(shí)在判斷if標(biāo)簽是否成立時(shí),答案是true,因此最終拼接出來一個(gè)錯(cuò)誤的SQL。
在3.4.5版本中,代碼中增加了context.getBindings().remove(item);在foreach標(biāo)簽解析完成后移除bindings中的參數(shù)映射。以下是源碼:
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
//foreach標(biāo)簽解析完成后,從bindings中移除item
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
到此這篇關(guān)于Mybatis #foreach中相同的變量名導(dǎo)致值覆蓋的問題解決的文章就介紹到這了,更多相關(guān)Mybatis #foreach相同變量名覆蓋內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Mybatis動(dòng)態(tài)SQL之if、choose、where、set、trim、foreach標(biāo)記實(shí)例詳解
- Mybatis中動(dòng)態(tài)SQL,if,where,foreach的使用教程詳解
- MyBatis的foreach語(yǔ)句詳解
- Mybatis foreach標(biāo)簽使用不當(dāng)導(dǎo)致異常的原因淺析
- mybatis3.4.6 批量更新 foreach 遍歷map 的正確姿勢(shì)詳解
- MyBatis傳入數(shù)組集合類并使用foreach遍歷
- mybatis-plus mapper中foreach循環(huán)操作代碼詳解(新增或修改)
- 解決mybatis批量更新(update foreach)失敗的問題
- mybatis foreach標(biāo)簽的使用詳解
相關(guān)文章
IntelliJ IDEA 2021.1 EAP 1 發(fā)布支持 Java 16 和 WSL 2
這篇文章主要介紹了IntelliJ IDEA 2021.1 EAP 1 發(fā)布支持 Java 16 和 WSL 2,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
JavaCV與FFmpeg音視頻流處理技巧總結(jié)大全
JavaCV是一個(gè)開源的Java接口,它為幾個(gè)著名的計(jì)算機(jī)視覺庫(kù)(如OpenCV、FFmpeg)提供了Java封裝,這篇文章主要給大家介紹了關(guān)于JavaCV與FFmpeg音視頻流處理技巧總結(jié)的相關(guān)資料,需要的朋友可以參考下2024-05-05
SpringBoot實(shí)現(xiàn)API接口的完整代碼
這篇文章主要給大家介紹了關(guān)于SpringBoot實(shí)現(xiàn)API接口的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
通過自定制LogManager實(shí)現(xiàn)程序完全自定義的logger
本章主要闡述怎么完全定制化LogManager來實(shí)現(xiàn)應(yīng)用程序完全自定制的logger,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
Java多線程實(shí)現(xiàn)之Callable詳解
這篇文章主要介紹了Java多線程實(shí)現(xiàn)之Callable詳解,Callable是一個(gè)接口,用于實(shí)現(xiàn)多線程,與實(shí)現(xiàn)Runnable類似,但是功能更強(qiáng)大,通過實(shí)現(xiàn)Callable接口,我們需要重寫call()方法,該方法可以在任務(wù)結(jié)束后提供一個(gè)返回值,需要的朋友可以參考下2023-08-08

