热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

easypoiexcelutil导入都是null_导入:Java实现大批量数据导入导出(100W以上)

阅读文本大概需要3分钟。来源:https:www.cnblogs.combarrywxxp10700221.html最近业务方有一个需求,需要一次导入超

阅读文本大概需要3分钟。

来源:https://www.cnblogs.com/barrywxx/p/10700221.html

最近业务方有一个需求,需要一次导入超过100万数据到系统数据库。可能大家首先会想,这么大的数据,干嘛通过程序去实现导入,为什么不直接通过SQL导入到数据库。

一、为什么一定要在代码实现

说说为什么不能通过SQL直接导入到数据库,而是通过程序实现:

1. 首先&#xff0c;这个导入功能开始提供页面导入&#xff0c;只是开始业务方保证的一次只有<3W的数据导入&#xff1b;

2. 其次&#xff0c;业务方导入的内容需要做校验&#xff0c;比如门店号&#xff0c;商品号等是否系统存在&#xff0c;需要程序校验&#xff1b;

3. 最后&#xff0c;业务方导入的都是编码&#xff0c;数据库中还要存入对应名称&#xff0c;方便后期查询&#xff0c;SQL导入也是无法实现的。

基于以上上三点&#xff0c;就无法直接通过SQL语句导入数据库。那就只能老老实实的想办法通过程序实现。

二、程序实现有以下技术难点

1. 一次读取这么大的数据量&#xff0c;肯定会导致服务器内存溢出&#xff1b;

2. 调用接口保存一次传输数据量太大&#xff0c;网络传输压力会很大&#xff1b;

3. 最终通过SQL一次批量插入&#xff0c;对数据库压力也比较大&#xff0c;如果业务同时操作这个表数据&#xff0c;很容易造成死锁。

三、解决思路

根据列举的技术难点我的解决思路是&#xff1a;

1. 既然一次读取整个导入文件&#xff0c;那就先将文件流上传到服务器磁盘&#xff0c;然后分批从磁盘读取(支持多线程读取)&#xff0c;这样就防止内存溢出&#xff1b;

2. 调用插入数据库接口也是根据分批读取的内容进行调用&#xff1b;

3. 分批插入数据到数据库。

四、具体实现代码

1. 流式上传文件到服务器磁盘

 略&#xff0c;一般Java上传就可以实现&#xff0c;这里就不贴出。

2. 多线程分批从磁盘读取

批量读取文件&#xff1a;

  1. import org.slf4j.Logger;

  2. import org.slf4j.LoggerFactory;

  3. import java.io.File;

  4. import java.io.FileNotFoundException;

  5. import java.io.RandomAccessFile;

  6. import java.nio.ByteBuffer;

  7. import java.nio.channels.FileChannel;

  8. /**

  9. * 类功能描述&#xff1a;批量读取文件

  10. *

  11. * &#64;author WangXueXing create at 19-3-14 下午6:47

  12. * &#64;version 1.0.0

  13. */

  14. public class BatchReadFile {

  15. private final Logger LOGGER &#61; LoggerFactory.getLogger(BatchReadFile.class);

  16. /**

  17. * 字符集UTF-8

  18. */

  19. public static final String CHARSET_UTF8 &#61; "UTF-8";

  20. /**

  21. * 字符集GBK

  22. */

  23. public static final String CHARSET_GBK &#61; "GBK";

  24. /**

  25. * 字符集gb2312

  26. */

  27. public static final String CHARSET_GB2312 &#61; "gb2312";

  28. /**

  29. * 文件内容分割符&#xff0d;逗号

  30. */

  31. public static final String SEPARATOR_COMMA &#61; ",";

  32. private int bufSize &#61; 1024;

  33. // 换行符

  34. private byte key &#61; "\n".getBytes()[0];

  35. // 当前行数

  36. private long lineNum &#61; 0;

  37. // 文件编码,默认为gb2312

  38. private String encode &#61; CHARSET_GB2312;

  39. // 具体业务逻辑监听器

  40. private ReaderFileListener readerListener;

  41. public void setEncode(String encode) {

  42. this.encode &#61; encode;

  43. }

  44. public void setReaderListener(ReaderFileListener readerListener) {

  45. this.readerListener &#61; readerListener;

  46. }

  47. /**

  48. * 获取准确开始位置

  49. * &#64;param file

  50. * &#64;param position

  51. * &#64;return

  52. * &#64;throws Exception

  53. */

  54. public long getStartNum(File file, long position) throws Exception {

  55. long startNum &#61; position;

  56. FileChannel fcin &#61; new RandomAccessFile(file, "r").getChannel();

  57. fcin.position(position);

  58. try {

  59. int cache &#61; 1024;

  60. ByteBuffer rBuffer &#61; ByteBuffer.allocate(cache);

  61. // 每次读取的内容

  62. byte[] bs &#61; new byte[cache];

  63. // 缓存

  64. byte[] tempBs &#61; new byte[0];

  65. while (fcin.read(rBuffer) !&#61; -1) {

  66. int rSize &#61; rBuffer.position();

  67. rBuffer.rewind();

  68. rBuffer.get(bs);

  69. rBuffer.clear();

  70. byte[] newStrByte &#61; bs;

  71. // 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面

  72. if (null !&#61; tempBs) {

  73. int tL &#61; tempBs.length;

  74. newStrByte &#61; new byte[rSize &#43; tL];

  75. System.arraycopy(tempBs, 0, newStrByte, 0, tL);

  76. System.arraycopy(bs, 0, newStrByte, tL, rSize);

  77. }

  78. // 获取开始位置之后的第一个换行符

  79. int endIndex &#61; indexOf(newStrByte, 0);

  80. if (endIndex !&#61; -1) {

  81. return startNum &#43; endIndex;

  82. }

  83. tempBs &#61; substring(newStrByte, 0, newStrByte.length);

  84. startNum &#43;&#61; 1024;

  85. }

  86. } finally {

  87. fcin.close();

  88. }

  89. return position;

  90. }

  91. /**

  92. * 从设置的开始位置读取文件&#xff0c;一直到结束为止。如果 end设置为负数,刚读取到文件末尾

  93. * &#64;param fullPath

  94. * &#64;param start

  95. * &#64;param end

  96. * &#64;throws Exception

  97. */

  98. public void readFileByLine(String fullPath, long start, long end) throws Exception {

  99. File fin &#61; new File(fullPath);

  100. if (!fin.exists()) {

  101. throw new FileNotFoundException("没有找到文件&#xff1a;" &#43; fullPath);

  102. }

  103. FileChannel fileChannel &#61; new RandomAccessFile(fin, "r").getChannel();

  104. fileChannel.position(start);

  105. try {

  106. ByteBuffer rBuffer &#61; ByteBuffer.allocate(bufSize);

  107. // 每次读取的内容

  108. byte[] bs &#61; new byte[bufSize];

  109. // 缓存

  110. byte[] tempBs &#61; new byte[0];

  111. String line;

  112. // 当前读取文件位置

  113. long nowCur &#61; start;

  114. while (fileChannel.read(rBuffer) !&#61; -1) {

  115. int rSize &#61; rBuffer.position();

  116. rBuffer.rewind();

  117. rBuffer.get(bs);

  118. rBuffer.clear();

  119. byte[] newStrByte;

  120. //去掉表头

  121. if(nowCur &#61;&#61; start){

  122. int firstLineIndex &#61; indexOf(bs, 0);

  123. int newByteLenth &#61; bs.length-firstLineIndex-1;

  124. newStrByte &#61; new byte[newByteLenth];

  125. System.arraycopy(bs, firstLineIndex&#43;1, newStrByte, 0, newByteLenth);

  126. } else {

  127. newStrByte &#61; bs;

  128. }

  129. // 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面

  130. if (null !&#61; tempBs && tempBs.length !&#61; 0) {

  131. int tL &#61; tempBs.length;

  132. newStrByte &#61; new byte[rSize &#43; tL];

  133. System.arraycopy(tempBs, 0, newStrByte, 0, tL);

  134. System.arraycopy(bs, 0, newStrByte, tL, rSize);

  135. }

  136. // 是否已经读到最后一位

  137. boolean isEnd &#61; false;

  138. nowCur &#43;&#61; bufSize;

  139. // 如果当前读取的位数已经比设置的结束位置大的时候&#xff0c;将读取的内容截取到设置的结束位置

  140. if (end > 0 && nowCur > end) {

  141. // 缓存长度 - 当前已经读取位数 - 最后位数

  142. int l &#61; newStrByte.length - (int) (nowCur - end);

  143. newStrByte &#61; substring(newStrByte, 0, l);

  144. isEnd &#61; true;

  145. }

  146. int fromIndex &#61; 0;

  147. int endIndex &#61; 0;

  148. // 每次读一行内容&#xff0c;以 key(默认为\n) 作为结束符

  149. while ((endIndex &#61; indexOf(newStrByte, fromIndex)) !&#61; -1) {

  150. byte[] bLine &#61; substring(newStrByte, fromIndex, endIndex);

  151. line &#61; new String(bLine, 0, bLine.length, encode);

  152. lineNum&#43;&#43;;

  153. // 输出一行内容&#xff0c;处理方式由调用方提供

  154. readerListener.outLine(line.trim(), lineNum, false);

  155. fromIndex &#61; endIndex &#43; 1;

  156. }

  157. // 将未读取完成的内容放到缓存中

  158. tempBs &#61; substring(newStrByte, fromIndex, newStrByte.length);

  159. if (isEnd) {

  160. break;

  161. }

  162. }

  163. // 将剩下的最后内容作为一行&#xff0c;输出&#xff0c;并指明这是最后一行

  164. String lineStr &#61; new String(tempBs, 0, tempBs.length, encode);

  165. readerListener.outLine(lineStr.trim(), lineNum, true);

  166. } finally {

  167. fileChannel.close();

  168. fin.deleteOnExit();

  169. }

  170. }

  171. /**

  172. * 查找一个byte[]从指定位置之后的一个换行符位置

  173. *

  174. * &#64;param src

  175. * &#64;param fromIndex

  176. * &#64;return

  177. * &#64;throws Exception

  178. */

  179. private int indexOf(byte[] src, int fromIndex) throws Exception {

  180. for (int i &#61; fromIndex; i

  181. if (src[i] &#61;&#61; key) {

  182. return i;

  183. }

  184. }

  185. return -1;

  186. }

  187. /**

  188. * 从指定开始位置读取一个byte[]直到指定结束位置为止生成一个全新的byte[]

  189. *

  190. * &#64;param src

  191. * &#64;param fromIndex

  192. * &#64;param endIndex

  193. * &#64;return

  194. * &#64;throws Exception

  195. */

  196. private byte[] substring(byte[] src, int fromIndex, int endIndex) throws Exception {

  197. int size &#61; endIndex - fromIndex;

  198. byte[] ret &#61; new byte[size];

  199. System.arraycopy(src, fromIndex, ret, 0, size);

  200. return ret;

  201. }

  202. }

以上是关键代码&#xff1a;利用FileChannel与ByteBuffer从磁盘中分批读取数据

多线程调用批量读取&#xff1a; 

  1. /**

  2. * 类功能描述: 线程读取文件

  3. *

  4. * &#64;author WangXueXing create at 19-3-14 下午6:51

  5. * &#64;version 1.0.0

  6. */

  7. public class ReadFileThread extends Thread {

  8. private ReaderFileListener processDataListeners;

  9. private String filePath;

  10. private long start;

  11. private long end;

  12. private Thread preThread;

  13. public ReadFileThread(ReaderFileListener processDataListeners,

  14. long start,long end,

  15. String file) {

  16. this(processDataListeners, start, end, file, null);

  17. }

  18. public ReadFileThread(ReaderFileListener processDataListeners,

  19. long start,long end,

  20. String file,

  21. Thread preThread) {

  22. this.setName(this.getName()&#43;"-ReadFileThread");

  23. this.start &#61; start;

  24. this.end &#61; end;

  25. this.filePath &#61; file;

  26. this.processDataListeners &#61; processDataListeners;

  27. this.preThread &#61; preThread;

  28. }

  29. &#64;Override

  30. public void run() {

  31. BatchReadFile readFile &#61; new BatchReadFile();

  32. readFile.setReaderListener(processDataListeners);

  33. readFile.setEncode(processDataListeners.getEncode());

  34. try {

  35. readFile.readFileByLine(filePath, start, end &#43; 1);

  36. if(this.preThread !&#61; null){

  37. this.preThread.join();

  38. }

  39. } catch (Exception e) {

  40. throw new RuntimeException(e);

  41. }

  42. }

  43. }

监听读取&#xff1a;

  1. import java.util.ArrayList;

  2. import java.util.List;

  3. /**

  4. * 类功能描述&#xff1a;读文件监听父类

  5. *

  6. * &#64;author WangXueXing create at 19-3-14 下午6:52

  7. * &#64;version 1.0.0

  8. */

  9. public abstract class ReaderFileListener {

  10. // 一次读取行数&#xff0c;默认为1000

  11. private int readColNum &#61; 1000;

  12. /**

  13. * 文件编码

  14. */

  15. private String encode;

  16. /**

  17. * 分批读取行列表

  18. */

  19. private List rowList &#61; new ArrayList<>();

  20. /**

  21. *其他参数

  22. */

  23. private T otherParams;

  24. /**

  25. * 每读取到一行数据&#xff0c;添加到缓存中

  26. * &#64;param lineStr 读取到的数据

  27. * &#64;param lineNum 行号

  28. * &#64;param over 是否读取完成

  29. * &#64;throws Exception

  30. */

  31. public void outLine(String lineStr, long lineNum, boolean over) throws Exception {

  32. if(null !&#61; lineStr && !lineStr.trim().equals("")){

  33. rowList.add(lineStr);

  34. }

  35. if (!over && (lineNum % readColNum &#61;&#61; 0)) {

  36. output(rowList);

  37. rowList &#61; new ArrayList<>();

  38. } else if (over) {

  39. output(rowList);

  40. rowList &#61; new ArrayList<>();

  41. }

  42. }

  43. /**

  44. * 批量输出

  45. *

  46. * &#64;param stringList

  47. * &#64;throws Exception

  48. */

  49. public abstract void output(List stringList) throws Exception;

  50. /**

  51. * 设置一次读取行数

  52. * &#64;param readColNum

  53. */

  54. protected void setReadColNum(int readColNum) {

  55. this.readColNum &#61; readColNum;

  56. }

  57. public String getEncode() {

  58. return encode;

  59. }

  60. public void setEncode(String encode) {

  61. this.encode &#61; encode;

  62. }

  63. public T getOtherParams() {

  64. return otherParams;

  65. }

  66. public void setOtherParams(T otherParams) {

  67. this.otherParams &#61; otherParams;

  68. }

  69. public List getRowList() {

  70. return rowList;

  71. }

  72. public void setRowList(List rowList) {

  73. this.rowList &#61; rowList;

  74. }

  75. }

实现监听读取并分批调用插入数据接口&#xff1a;

  1. import com.today.api.finance.ImportServiceClient;

  2. import com.today.api.finance.request.ImportRequest;

  3. import com.today.api.finance.response.ImportResponse;

  4. import com.today.api.finance.service.ImportService;

  5. import com.today.common.Constants;

  6. import com.today.domain.StaffSimpInfo;

  7. import com.today.util.EmailUtil;

  8. import com.today.util.UserSessionHelper;

  9. import com.today.util.readfile.ReadFile;

  10. import com.today.util.readfile.ReadFileThread;

  11. import com.today.util.readfile.ReaderFileListener;

  12. import org.slf4j.Logger;

  13. import org.slf4j.LoggerFactory;

  14. import org.springframework.beans.factory.annotation.Value;

  15. import org.springframework.stereotype.Service;

  16. import org.springframework.util.StringUtils;

  17. import java.io.File;

  18. import java.io.FileInputStream;

  19. import java.util.ArrayList;

  20. import java.util.Arrays;

  21. import java.util.List;

  22. import java.util.concurrent.FutureTask;

  23. import java.util.stream.Collectors;

  24. /**

  25. * 类功能描述&#xff1a;报表导入服务实现

  26. *

  27. * &#64;author WangXueXing create at 19-3-19 下午1:43

  28. * &#64;version 1.0.0

  29. */

  30. &#64;Service

  31. public class ImportReportServiceImpl extends ReaderFileListener {

  32. private final Logger LOGGER &#61; LoggerFactory.getLogger(ImportReportServiceImpl.class);

  33. &#64;Value("${READ_COL_NUM_ONCE}")

  34. private String readColNum;

  35. &#64;Value("${REPORT_IMPORT_RECEIVER}")

  36. private String reportImportReceiver;

  37. /**

  38. * 财务报表导入接口

  39. */

  40. private ImportService service &#61; new ImportServiceClient();

  41. /**

  42. * 读取文件内容

  43. * &#64;param file

  44. */

  45. public void readTxt(File file, ImportRequest importRequest) throws Exception {

  46. this.setOtherParams(importRequest);

  47. ReadFile readFile &#61; new ReadFile();

  48. try(FileInputStream fis &#61; new FileInputStream(file)){

  49. int available &#61; fis.available();

  50. long maxThreadNum &#61; &#xff13;L;

  51. // 线程粗略开始位置

  52. long i &#61; available / maxThreadNum;

  53. this.setRowList(new ArrayList<>());

  54. StaffSimpInfo staffSimpInfo &#61; ((StaffSimpInfo)UserSessionHelper.getCurrentUserInfo().getData());

  55. String finalReportReceiver &#61; getEmail(staffSimpInfo.getEmail(), reportImportReceiver);

  56. this.setReadColNum(Integer.parseInt(readColNum));

  57. this.setEncode(ReadFile.CHARSET_GB2312);

  58. //这里单独使用一个线程是为了当maxThreadNum大于1的时候&#xff0c;统一管理这些线程

  59. new Thread(()->{

  60. Thread preThread &#61; null;

  61. FutureTask futureTask &#61; null ;

  62. try {

  63. for (long j &#61; 0; j

  64. //计算精确开始位置

  65. long startNum &#61; j &#61;&#61; 0 ? 0 : readFile.getStartNum(file, i * j);

  66. long endNum &#61; j &#43; 1

  67. //具体监听实现

  68. preThread &#61; new ReadFileThread(this, startNum, endNum, file.getPath(), preThread);

  69. futureTask &#61; new FutureTask(preThread, new Object());

  70. futureTask.run();

  71. }

  72. if(futureTask.get() !&#61; null) {

  73. EmailUtil.sendEmail(EmailUtil.REPORT_IMPORT_EMAIL_PREFIX, finalReportReceiver, "导入报表成功", "导入报表成功" ); //todo 等文案

  74. }

  75. } catch (Exception e){

  76. futureTask.cancel(true);

  77. try {

  78. EmailUtil.sendEmail(EmailUtil.REPORT_IMPORT_EMAIL_PREFIX, finalReportReceiver, "导入报表失败", e.getMessage());

  79. } catch (Exception e1){

  80. //ignore

  81. LOGGER.error("发送邮件失败", e1);

  82. }

  83. LOGGER.error("导入报表类型:"&#43;importRequest.getReportType()&#43;"失败", e);

  84. } finally {

  85. futureTask.cancel(true);

  86. }

  87. }).start();

  88. }

  89. }

  90. private String getEmail(String infoEmail, String reportImportReceiver){

  91. if(StringUtils.isEmpty(infoEmail)){

  92. return reportImportReceiver;

  93. }

  94. return infoEmail;

  95. }

  96. /**

  97. * 每批次调用导入接口

  98. * &#64;param stringList

  99. * &#64;throws Exception

  100. */

  101. &#64;Override

  102. public void output(List stringList) throws Exception {

  103. ImportRequest importRequest &#61; this.getOtherParams();

  104. List> dataList &#61; stringList.stream()

  105. .map(x->Arrays.asList(x.split(ReadFile.SEPARATOR_COMMA)).stream().map(String::trim).collect(Collectors.toList()))

  106. .collect(Collectors.toList());

  107. LOGGER.info("上传数据:{}", dataList);

  108. importRequest.setDataList(dataList);

  109. // LOGGER.info("request对象&#xff1a;{}",importRequest, "request增加请求字段&#xff1a;{}", importRequest.data);

  110. ImportResponse importResponse &#61; service.batchImport(importRequest);

  111. LOGGER.info("&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;SUCESS_CODE&#61;&#61;&#61;&#61;&#61;&#61;&#61;"&#43;importResponse.getCode());

  112. //导入错误,输出错误信息

  113. if(!Constants.SUCESS_CODE.equals(importResponse.getCode())){

  114. LOGGER.error("导入报表类型:"&#43;importRequest.getReportType()&#43;"失败","返回码为&#xff1a;", importResponse.getCode() ,"返回信息&#xff1a;",importResponse.getMessage());

  115. throw new RuntimeException("导入报表类型:"&#43;importRequest.getReportType()&#43;"失败"&#43;"返回码为&#xff1a;"&#43; importResponse.getCode() &#43;"返回信息&#xff1a;"&#43;importResponse.getMessage());

  116. }

  117. // if(importResponse.data !&#61; null && importResponse.data.get().get("batchImportFlag")!&#61;null) {

  118. // LOGGER.info("eywa-service请求batchImportFlag不为空");

  119. // }

  120. importRequest.setData(importResponse.data);

  121. }

  122. }

注意&#xff1a;第53行代码&#xff1a;long maxThreadNum &#61; &#xff13;L;
就是设置分批读取磁盘文件的线程数&#xff0c;我设置为&#xff13;&#xff0c;大家不要设置太大&#xff0c;不然多个线程读取到内存&#xff0c;也会造成服务器内存溢出。

以上所有批次的批量读取并调用插入接口都成功发送邮件通知给导入人&#xff0c;任何一个批次失败直接发送失败邮件。 

数据库分批插入数据&#xff1a;

  1. /**

  2. * 批量插入非联机第三方导入账单

  3. * &#64;param dataList

  4. */

  5. def insertNonOnlinePayment(dataList: List[NonOnlineSourceData]) : Unit &#61; {

  6. if (dataList.nonEmpty) {

  7. CheckAccountDataSource.mysqlData.withConnection { conn &#61;>

  8. val sql &#61;

  9. s""" INSERT INTO t_pay_source_data

  10. (store_code,

  11. store_name,

  12. source_date,

  13. order_type,

  14. trade_type,

  15. third_party_payment_no,

  16. business_type,

  17. business_amount,

  18. trade_time,

  19. created_at,

  20. updated_at)

  21. VALUES (?,?,?,?,?,?,?,?,?,NOW(),NOW())"""

  22. conn.setAutoCommit(false)

  23. var stmt &#61; conn.prepareStatement(sql)

  24. var i &#61; 0

  25. dataList.foreach { x &#61;>

  26. stmt.setString(1, x.storeCode)

  27. stmt.setString(2, x.storeName)

  28. stmt.setString(3, x.sourceDate)

  29. stmt.setInt(4, x.orderType)

  30. stmt.setInt(5, x.tradeType)

  31. stmt.setString(6, x.tradeNo)

  32. stmt.setInt(7, x.businessType)

  33. stmt.setBigDecimal(8, x.businessAmount.underlying())

  34. stmt.setString(9, x.tradeTime.getOrElse(null))

  35. stmt.addBatch()

  36. if ((i % 5000 &#61;&#61; 0) && (i !&#61; 0)) { //分批提交

  37. stmt.executeBatch

  38. conn.commit

  39. conn.setAutoCommit(false)

  40. stmt &#61; conn.prepareStatement(sql)

  41. }

  42. i &#43;&#61; 1

  43. }

  44. stmt.executeBatch()

  45. conn.commit()

  46. }

  47. }

  48. }

 以上代码实现每5000 行提交一次批量插入&#xff0c;防止一次提较数据库的压力。

往期精彩

01 漫谈发版哪些事&#xff0c;好课程推荐

02 Linux的常用最危险的命令

03 精讲Spring Boot—入门&#43;进阶&#43;实例

04 优秀的Java程序员必须了解的GC哪些

05 互联网支付系统整体架构详解

关注我

每天进步一点点

d913193339d39c2465012ef40d92364d.png

喜欢&#xff01;在看☟



推荐阅读
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 深入解析动态代理模式:23种设计模式之三
    在设计模式中,动态代理模式是应用最为广泛的一种代理模式。它允许我们在运行时动态创建代理对象,并在调用方法时进行增强处理。本文将详细介绍动态代理的实现机制及其应用场景。 ... [详细]
  • 本文详细介绍了流编辑器sed中的G、H、g、h命令,探讨了它们的工作原理及应用场景。通过实例解析和图解分析,帮助读者掌握这些高级命令的使用方法。 ... [详细]
  • 深入理解Vue.js:从入门到精通
    本文详细介绍了Vue.js的基础知识、安装方法、核心概念及实战案例,帮助开发者全面掌握这一流行的前端框架。 ... [详细]
  • 本文介绍了如何在 Node.js 中使用 `setDefaultEncoding` 方法为可写流设置默认编码,并提供了详细的语法说明和示例代码。 ... [详细]
  • Redux入门指南
    本文介绍Redux的基本概念和工作原理,帮助初学者理解如何使用Redux管理应用程序的状态。Redux是一个用于JavaScript应用的状态管理库,特别适用于React项目。 ... [详细]
  • LeetCode 690:计算员工的重要性评分
    在解决LeetCode第690题时,我记录了详细的解题思路和方法。该问题要求根据员工的ID计算其重要性评分,包括直接和间接下属的重要性。本文将深入探讨如何使用哈希表(Map)来高效地实现这一目标。 ... [详细]
  • 本文介绍如何从字符串中移除大写、小写、特殊、数字和非数字字符,并提供了多种编程语言的实现示例。 ... [详细]
  • 本文详细介绍如何使用 HTML5 和 JavaScript 实现一个交互式的画板功能。通过具体代码示例,帮助读者理解 Canvas API 的基本用法及其在绘图应用中的实际应用。 ... [详细]
  • 本文详细介绍了 Java 中 org.geotools.data.shapefile.ShapefileDataStore 类的 getCurrentTypeName() 方法,并提供了多个代码示例,帮助开发者更好地理解和使用该方法。 ... [详细]
  • 深入解析SpringMVC核心组件:DispatcherServlet的工作原理
    本文详细探讨了SpringMVC的核心组件——DispatcherServlet的运作机制,旨在帮助有一定Java和Spring基础的开发人员理解HTTP请求是如何被映射到Controller并执行的。文章将解答以下问题:1. HTTP请求如何映射到Controller;2. Controller是如何被执行的。 ... [详细]
  • 本文介绍 Java 中如何使用 Year 类的 atMonth 方法将年份和月份组合成 YearMonth 对象,并提供代码示例。 ... [详细]
  • 本文详细介绍了如何在Kendo UI for jQuery的数据管理组件中,将行标题字段呈现为锚点(即可点击链接),帮助开发人员更高效地实现这一功能。通过具体的代码示例和解释,即使是新手也能轻松掌握。 ... [详细]
  • 在尝试使用C# Windows Forms客户端通过SignalR连接到ASP.NET服务器时,遇到了内部服务器错误(500)。本文将详细探讨问题的原因及解决方案。 ... [详细]
  • Symfony是一个功能强大的PHP框架,以其依赖注入(DI)特性著称。许多流行的PHP框架如Drupal和Laravel的核心组件都基于Symfony构建。本文将详细介绍Symfony的安装方法及其基本使用。 ... [详细]
author-avatar
用户7kxpkjs2ol
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有