Spring實(shí)現(xiàn)文件上傳的配置詳解
添加依賴
主要用來解析request請求流,獲取文件字段名、上傳文件名、content-type、headers等內(nèi)容組裝成FileItem
<!--添加fileupload依賴-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>構(gòu)建單例bean
CommonsMultipartResolver,將request請求從類型HttpServletRequest轉(zhuǎn)化成MultipartHttpServletRequest,從MultipartHttpServletRequest可以獲取上傳文件的各種信息文件名、文件流等內(nèi)容
注意:該bean的beanName要寫成multipartResolver,否則無法獲取到該bean
@Bean
public CommonsMultipartResolver multipartResolver() {
CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
// 上傳限制最大字節(jié)數(shù) -1表示沒限制
commonsMultipartResolver.setMaxUploadSize(-1);
// 每個(gè)文件限制最大字節(jié)數(shù) -1表示沒限制
commonsMultipartResolver.setMaxUploadSizePerFile(-1);
commonsMultipartResolver.setDefaultEncoding(StandardCharsets.UTF_8.name());
return commonsMultipartResolver;
}
#DispatcherServlet
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
private void initMultipartResolver(ApplicationContext context) {
try {
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
}
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
this.multipartResolver = null;
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate MultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME +
"': no multipart request handling provided");
}
}
}校驗(yàn)請求
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// multipartResolver不為空 且 request請求頭中的content-type以multipart/開頭
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
"this typically results from an additional MultipartFilter in web.xml");
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
// 解析請求
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}解析請求
@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
MultipartParsingResult parsingResult = parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = determineEncoding(request);
// 獲取FileUpload實(shí)例
FileUpload fileUpload = prepareFileUpload(encoding);
try {
// 將request請求解析成FileItem
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);
}
catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
}
catch (FileUploadBase.FileSizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
}
catch (FileUploadException ex) {
throw new MultipartException("Failed to parse multipart servlet request", ex);
}
}
// ctx->將request進(jìn)行了包裝
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<FileItem>();
boolean successful = false;
try {
// 通過ctx構(gòu)建FileItem流的迭代器
FileItemIterator iter = getItemIterator(ctx);
// FileItemFactory創(chuàng)建FileItem的工廠對象
FileItemFactory fac = getFileItemFactory();
if (fac == null) {
throw new NullPointerException("No FileItemFactory has been set.");
}
// 判斷是否itemValid是否為true,是否有可讀文件
while (iter.hasNext()) {
final FileItemStream item = iter.next();
// Don't use getName() here to prevent an InvalidFileNameException.
// 文件名稱
final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
// 構(gòu)建FileItem
FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
// 將FileItemStreamImpl流拷貝到fileItem的輸出流中(系統(tǒng)會(huì)自建文件)
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(format("Processing of %s request failed. %s",
MULTIPART_FORM_DATA, e.getMessage()), e);
}
final FileItemHeaders fih = item.getHeaders();
fileItem.setHeaders(fih);
}
successful = true;
return items;
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new FileUploadException(e.getMessage(), e);
} finally {
if (!successful) {
for (FileItem fileItem : items) {
try {
fileItem.delete();
} catch (Throwable e) {
// ignore it
}
}
}
}
}
#解析獲取到的fileItems
protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<>();
Map<String, String[]> multipartParameters = new HashMap<>();
Map<String, String> multipartParameterContentTypes = new HashMap<>();
// Extract multipart files and multipart parameters.
for (FileItem fileItem : fileItems) {
// 是否是表單字段(下面的解析可以看到構(gòu)建時(shí)該字段傳參 fileName == null),也就是文件名是否為空
if (fileItem.isFormField()) {
String value;
String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
try {
value = fileItem.getString(partEncoding);
}
catch (UnsupportedEncodingException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
"' with encoding '" + partEncoding + "': using platform default");
}
value = fileItem.getString();
}
String[] curParam = multipartParameters.get(fileItem.getFieldName());
if (curParam == null) {
// simple form field
multipartParameters.put(fileItem.getFieldName(), new String[] {value});
}
else {
// array of simple form fields
String[] newParam = StringUtils.addStringToArray(curParam, value);
multipartParameters.put(fileItem.getFieldName(), newParam);
}
multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
}
else {
// multipart file field 構(gòu)建MultipartFile
CommonsMultipartFile file = createMultipartFile(fileItem);
// 以文件字段名為key (files)
multipartFiles.add(file.getName(), file);
if (logger.isDebugEnabled()) {
logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() +
" bytes with original filename [" + file.getOriginalFilename() + "], stored " +
file.getStorageDescription());
}
}
}
return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
}主要邏輯是這行代碼FileItemIterator iter = getItemIterator(ctx);,F(xiàn)ileItem流迭代器的構(gòu)造
#構(gòu)造方法
FileItemIteratorImpl(RequestContext ctx)
throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException("ctx parameter");
}
// 獲取request的content-type,需要以multipart/ 開頭
String contentType = ctx.getContentType();
if ((null == contentType)
|| (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
throw new InvalidContentTypeException(
format("the request doesn't contain a %s or %s stream, content type header is %s",
MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
}
// 獲取request的輸入流
InputStream input = ctx.getInputStream();
// 獲取內(nèi)容長度 content-length 從request中取
@SuppressWarnings("deprecation") // still has to be backward compatible
final int contentLengthInt = ctx.getContentLength();
// 通過request.getHeader()取
final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())
// Inline conditional is OK here CHECKSTYLE:OFF
? ((UploadContext) ctx).contentLength()
: contentLengthInt;
// CHECKSTYLE:ON
// sizeMax限制流大小 -1則不限制
if (sizeMax >= 0) {
if (requestSize != -1 && requestSize > sizeMax) {
throw new SizeLimitExceededException(
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
Long.valueOf(requestSize), Long.valueOf(sizeMax)),
requestSize, sizeMax);
}
input = new LimitedInputStream(input, sizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
FileUploadException ex = new SizeLimitExceededException(
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
Long.valueOf(pCount), Long.valueOf(pSizeMax)),
pCount, pSizeMax);
throw new FileUploadIOException(ex);
}
};
}
// 獲取字符編碼
String charEncoding = headerEncoding;
if (charEncoding == null) {
charEncoding = ctx.getCharacterEncoding();
}
// 通過content-type = multipart/form-data; boundary=--------------------------205940049223747054037567
// 獲取boundary的值分隔符(一串隨機(jī)字符?)并轉(zhuǎn)化為字節(jié)數(shù)組
boundary = getBoundary(contentType);
if (boundary == null) {
throw new FileUploadException("the request was rejected because no multipart boundary was found");
}
// 進(jìn)度更新器
notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
try {
// 構(gòu)建多元流
multi = new MultipartStream(input, boundary, notifier);
} catch (IllegalArgumentException iae) {
throw new InvalidContentTypeException(
format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);
}
// 設(shè)置請求頭編碼
multi.setHeaderEncoding(charEncoding);
// 跳過序言
skipPreamble = true;
// 開始找第一個(gè)文件項(xiàng)目
findNextItem();
}接著再來看下MultipartStream的構(gòu)建
#MultipartStream構(gòu)造
public MultipartStream(InputStream input, // request輸入流
byte[] boundary, // 邊界 字節(jié)數(shù)組
int bufSize, // 緩沖區(qū)大小 默認(rèn)4096
ProgressNotifier pNotifier) {
if (boundary == null) {
throw new IllegalArgumentException("boundary may not be null");
}
// We prepend CR/LF to the boundary to chop trailing CR/LF from
// body-data tokens. CR 回車\r LF 換行\(zhòng)n
// protected static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH};
this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
// 緩沖區(qū)大小判斷
if (bufSize < this.boundaryLength + 1) {
throw new IllegalArgumentException(
"The buffer size specified for the MultipartStream is too small");
}
this.input = input;
// 重新確定緩沖區(qū)大小
this.bufSize = Math.max(bufSize, boundaryLength * 2);
// 創(chuàng)建緩沖區(qū) 用來從讀inputStream 接受數(shù)據(jù)
this.buffer = new byte[this.bufSize];
this.notifier = pNotifier;
// 邊界數(shù)組
this.boundary = new byte[this.boundaryLength];
this.keepRegion = this.boundary.length;
// 將BOUNDARY_PREFIX數(shù)組和入?yún)oundary數(shù)組的內(nèi)容按序復(fù)制到新的boundary中
System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
BOUNDARY_PREFIX.length);
System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
boundary.length);
// head和tail為緩沖區(qū)操作的索引
// 0 <= head < bufSize
// 0 <= tail <= bufSize
head = 0;
tail = 0;
}接著看findNextItem方法,找第一個(gè)文件項(xiàng)目
/**
* Called for finding the next item, if any.
*
* @return True, if an next item was found, otherwise false.
* @throws IOException An I/O error occurred.
*/
private boolean findNextItem() throws IOException {
if (eof) {
return false;
}
// 開始為null
if (currentItem != null) {
currentItem.close();
currentItem = null;
}
for (;;) {
boolean nextPart;
if (skipPreamble) {
// 丟棄直到邊界分隔符的所有數(shù)據(jù) 再讀取邊界
nextPart = multi.skipPreamble();
} else {
// 直接讀取邊界
nextPart = multi.readBoundary();
}
if (!nextPart) {
if (currentFieldName == null) {
// Outer multipart terminated -> No more data
eof = true;
return false;
}
// Inner multipart terminated -> Return to parsing the outer
multi.setBoundary(boundary);
currentFieldName = null;
continue;
}
// 解析頭部 multi.readHeaders()從緩沖區(qū)解析到所有請求頭的字符串
// 接著getParsedHeaders將字符串按\r\n分隔,因?yàn)槊恳恍袛?shù)據(jù)都是一個(gè)請求頭內(nèi)容
FileItemHeaders headers = getParsedHeaders(multi.readHeaders());
if (currentFieldName == null) {
// We're parsing the outer multipart
// 獲取上傳文件字段參數(shù)名name = files
String fieldName = getFieldName(headers);
if (fieldName != null) {
String subContentType = headers.getHeader(CONTENT_TYPE); // img/jpeg
if (subContentType != null
&& subContentType.toLowerCase(Locale.ENGLISH)
.startsWith(MULTIPART_MIXED)) {
currentFieldName = fieldName;
// Multiple files associated with this field name
byte[] subBoundary = getBoundary(subContentType);
multi.setBoundary(subBoundary);
skipPreamble = true;
continue;
}
// 文件名 IMG_0908.JPG
String fileName = getFileName(headers);
// 根據(jù)字段名、文件名、請求頭等構(gòu)建FileItemStream對象
currentItem = new FileItemStreamImpl(fileName,
fieldName, headers.getHeader(CONTENT_TYPE),
fileName == null, getContentLength(headers));
// 設(shè)置請求頭
currentItem.setHeaders(headers);
// ++items
notifier.noteItem();
// 當(dāng)期有可用item
itemValid = true;
return true;
}
} else {
String fileName = getFileName(headers);
if (fileName != null) {
currentItem = new FileItemStreamImpl(fileName,
currentFieldName,
headers.getHeader(CONTENT_TYPE),
false, getContentLength(headers));
currentItem.setHeaders(headers);
notifier.noteItem();
itemValid = true;
return true;
}
}
multi.discardBodyData();
}
}下圖為緩沖區(qū)數(shù)據(jù),首行為boundary分隔符內(nèi)容也就是上文提到的----加一串計(jì)算出來的隨機(jī)字符,\r\n后接著為content-Disposition和content-type請求頭,可以看到 HEADER_SEPARATOR頭部分隔符\r\n\r\n 和分隔符之前的為請求頭數(shù)據(jù)
另外boundary字節(jié)數(shù)組對應(yīng)的內(nèi)容為--------------------------031262929361076583805179,下圖的首行內(nèi)容比其多兩個(gè)-

下圖為解析完后的頭部

接下來再看下FileItemStreamImpl的構(gòu)造過程,比較簡單
#FileItemStreamImpl構(gòu)造方法
FileItemStreamImpl(String pName, String pFieldName,
String pContentType, boolean pFormField,
long pContentLength) throws IOException {
name = pName;
fieldName = pFieldName;
contentType = pContentType;
formField = pFormField;
// 創(chuàng)建itemStream流 本質(zhì)上是從request的inputstream獲取數(shù)據(jù)
// 從head位置再開始找boundary邊界分隔符,若找到將邊界的前一個(gè)索引賦值給pos變量,并且當(dāng)前文件可讀字符數(shù)為pos - head
final ItemInputStream itemStream = multi.newInputStream();
InputStream istream = itemStream;
// 若文件大小限制,超出長度會(huì)拋出異常
if (fileSizeMax != -1) {
if (pContentLength != -1
&& pContentLength > fileSizeMax) {
FileSizeLimitExceededException e =
new FileSizeLimitExceededException(
format("The field %s exceeds its maximum permitted size of %s bytes.",
fieldName, Long.valueOf(fileSizeMax)),
pContentLength, fileSizeMax);
e.setFileName(pName);
e.setFieldName(pFieldName);
throw new FileUploadIOException(e);
}
istream = new LimitedInputStream(istream, fileSizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
itemStream.close(true);
FileSizeLimitExceededException e =
new FileSizeLimitExceededException(
format("The field %s exceeds its maximum permitted size of %s bytes.",
fieldName, Long.valueOf(pSizeMax)),
pCount, pSizeMax);
e.setFieldName(fieldName);
e.setFileName(name);
throw new FileUploadIOException(e);
}
};
}
stream = istream;
}再來看看ItemInputStream,上面的FileItemStreamImpl對象有這個(gè)類型參數(shù),主要用來獲取請求流的,因?yàn)镮temInputStream類是MultipartStream的內(nèi)部類,能夠調(diào)用MultipartStream中的input流。
ItemInputStream
public class ItemInputStream extends InputStream implements Closeable {
// 目前已經(jīng)讀取的字節(jié)數(shù)
private long total;
// 必須保持的字節(jié)數(shù)可能是分隔符boundary的一部分
private int pad;
// 緩沖區(qū)的當(dāng)前偏移
private int pos;
// stream流是否關(guān)閉
private boolean closed;
// 構(gòu)造方法
ItemInputStream() {
findSeparator();
}
// 尋找邊界分隔符boundary的前一個(gè)索引
private void findSeparator() {
pos = MultipartStream.this.findSeparator();
if (pos == -1) {
if (tail - head > keepRegion) {
pad = keepRegion;
} else {
pad = tail - head;
}
}
}
// 可讀取字節(jié)數(shù)
@Override
public int available() throws IOException {
// 可讀=尾-首-邊界長度
if (pos == -1) {
return tail - head - pad;
}
// pos !=-1 說明pos后面是邊界了 只能讀到這個(gè)邊界之前的數(shù)據(jù)
// 可讀 = 邊界前的最后一個(gè)索引 - 首
return pos - head;
}
private static final int BYTE_POSITIVE_OFFSET = 256;
// 讀取stream流的下一個(gè)字符
@Override
public int read() throws IOException {
if (closed) {
throw new FileItemStream.ItemSkippedException();
}
if (available() == 0 && makeAvailable() == 0) {
return -1;
}
++total;
int b = buffer[head++];
if (b >= 0) {
return b;
}
// 如果負(fù)的 加上256
return b + BYTE_POSITIVE_OFFSET;
}
// 讀取字節(jié)到給定的緩沖區(qū)b中
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new FileItemStream.ItemSkippedException();
}
if (len == 0) {
return 0;
}
int res = available();
if (res == 0) {
res = makeAvailable();
if (res == 0) {
return -1;
}
}
res = Math.min(res, len);
System.arraycopy(buffer, head, b, off, res);
// head加偏移
head += res;
total += res;
return res;
}
// 關(guān)閉輸入流
@Override
public void close() throws IOException {
close(false);
}
/**
* Closes the input stream.
*
* @param pCloseUnderlying Whether to close the underlying stream
* (hard close)
* @throws IOException An I/O error occurred.
*/
public void close(boolean pCloseUnderlying) throws IOException {
if (closed) {
return;
}
if (pCloseUnderlying) {
closed = true;
input.close();
} else {
for (;;) {
int av = available();
if (av == 0) {
av = makeAvailable();
if (av == 0) {
break;
}
}
skip(av);
}
}
closed = true;
}
// 跳過緩沖區(qū)中給定長度的字節(jié)
@Override
public long skip(long bytes) throws IOException {
if (closed) {
throw new FileItemStream.ItemSkippedException();
}
int av = available();
if (av == 0) {
av = makeAvailable();
if (av == 0) {
return 0;
}
}
long res = Math.min(av, bytes);
head += res;
return res;
}
// 試圖讀取更多的數(shù)據(jù),返回可讀字節(jié)數(shù)
private int makeAvailable() throws IOException {
if (pos != -1) {
return 0;
}
// 將數(shù)據(jù)移到緩沖區(qū)的開頭,舍棄邊界
total += tail - head - pad;
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
// Refill buffer with new data.
head = 0;
tail = pad;
for (;;) {
// 讀取tail位置開始讀 bufSize-tail長度的字節(jié)到buffer緩沖區(qū)中
int bytesRead = input.read(buffer, tail, bufSize - tail);
if (bytesRead == -1) {
// The last pad amount is left in the buffer.
// Boundary can't be in there so signal an error
// condition.
final String msg = "Stream ended unexpectedly";
throw new MalformedStreamException(msg);
}
if (notifier != null) {
notifier.noteBytesRead(bytesRead);
}
// tail加偏移
tail += bytesRead;
// 再嘗試找boundary邊界,賦值pos -1
findSeparator();
int av = available();
// 返回可讀字節(jié)數(shù)
if (av > 0 || pos != -1) {
return av;
}
}
}
// 判斷流是否關(guān)閉
public boolean isClosed() {
return closed;
}
}接受請求
請求解析完成后就可以以文件對象接收了,參數(shù)類型為MultipartFile,可強(qiáng)轉(zhuǎn)為CommonsMultipartFile,參數(shù)名需要與上傳文件的fieldName相對應(yīng)或者也可以用@RequestParam注解指定參數(shù)名
@RequestMapping(value = "file/upload", method = RequestMethod.POST)
@ResponseBody
public Object uploadFile(MultipartFile[] files, HttpServletRequest request, HttpServletResponse response) throws IOException {
....... 上傳邏輯
return CommonResult.succ("上傳成功");
}可以用postman測試,content-type的boundary顯示是請求發(fā)送時(shí)計(jì)算,不知道怎么算的反正是一串隨機(jī)數(shù),是用來分隔多個(gè)文件內(nèi)容的,在源碼中可以看到不能缺失否則解析過程中會(huì)報(bào)錯(cuò)


以上就是Spring實(shí)現(xiàn)文件上傳的配置詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring文件上傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深度解析Java中volatile的內(nèi)存語義實(shí)現(xiàn)以及運(yùn)用場景
這篇文章主要介紹了Java中volatile的內(nèi)存語義實(shí)現(xiàn)以及運(yùn)用場景,通過JVM的機(jī)制來分析volatile關(guān)鍵字在線程編程中的作用,需要的朋友可以參考下2015-12-12
SpringBoot項(xiàng)目的配置文件中設(shè)置server.port不生效問題
這篇文章主要介紹了SpringBoot項(xiàng)目的配置文件中設(shè)置server.port不生效問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
SpringBoot集成Beetl后統(tǒng)一處理頁面異常的方法
這篇文章主要介紹了SpringBoot集成Beetl后統(tǒng)一處理頁面異常的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
java中實(shí)現(xiàn)創(chuàng)建目錄與創(chuàng)建文件的操作實(shí)例
用Java創(chuàng)建文件或目錄非常簡單,下面這篇文章主要給大家介紹了關(guān)于java中實(shí)現(xiàn)創(chuàng)建目錄與創(chuàng)建文件的操作實(shí)例,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01
Mybatis-Plus處理Mysql?Json類型字段的詳細(xì)教程
這篇文章主要給大家介紹了關(guān)于Mybatis-Plus處理Mysql?Json類型字段的詳細(xì)教程,Mybatis-Plus可以很方便地處理JSON字段,在實(shí)體類中可以使用@JSONField注解來標(biāo)記JSON字段,同時(shí)在mapper.xml中使用json函數(shù)來操作JSON字段,需要的朋友可以參考下2024-01-01
Java實(shí)現(xiàn)多數(shù)據(jù)源的幾種方式總結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于Java實(shí)現(xiàn)多數(shù)據(jù)源的幾種方式,最近項(xiàng)目中的工作流需要查詢多個(gè)數(shù)據(jù)源的數(shù)據(jù),數(shù)據(jù)源可能是不同種類的,需要的朋友可以參考下2023-08-08

