经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » MySQL » 查看文章
EasyExcel处理Mysql百万数据的导入导出案例,秒级效率,拿来即用!
来源:cnblogs  作者:JavaBuild  时间:2024/5/13 8:55:17  对本文有异议

一、写在开头

今天终于更新新专栏 《EfficientFarm》 的第二篇博文啦,本文主要来记录一下对于EasyExcel的高效应用,包括对MySQL数据库百万级数据量的导入与导出操作,以及性能的优化(争取做到秒级性能!)。


二、如何做技术选型

其实在市面上我们有很多常用的excel操作依赖库,除了EasyExcel之外,还有EasyPOI、JXL、JXLS等等,他们各有千秋,依赖重点不同,我们在做技术选型的时候,要根据自己的需求去做针对性选择,下面我们列举了这几种常见技术的特点对比

技术方案 优点 缺点
EasyExcel 简单易用,API设计友好;
高效处理大量数据;
支持自定义样式和格式化器等功能
不支持老版本 Excel 文件 (如 xls 格式)
POI Apache开源项目,稳定性高,EasyPOI基于它开发的,特点类似,进行了功能增强,这里不单独列举;
支持多种格式(XLS、XLSX等);
可以读写复杂表格(如带有合并单元格或图表的表格)
API使用较为繁琐;对于大数据量可能会存在性能问题
Jxls 具备良好的模板引擎机制,支持通过模板文件生成 Excel 表格;
提供了可视化设计器来快速创建报告模板
性能相对其他两个方案稍弱一些;
模板与代码耦合度较高。

而本文中主要针对的是大数据量的导入与导出,因此,我们果断的选择了EasyExcel技术进行实现。


三、应用场景模拟

假设我们在开发中接到了一个需求要求我们做一个功能:

1、导出商城中所有的用户信息,由于用户规模达到了百万级,导出等待时间不可太长
2、允许通过规定的excel模板进行百万级用户信息的初始化(系统迁移时会发生)。

拿到这个需求后,经过技术选型EasyExcel后,我们在心里有个大概的构想了,大概可以分三个内容 :“模板下载”、“上传数据”、“下载数据”

想好这些后,我们就可以开整了!???


四、数据准备

在数据准备阶段,我们应该做如下几点:

1. 在数据库中创建一个用户信息表User;

  1. -- 如果存在表先删除
  2. drop table if exists `user`;
  3. --建表语句
  4. CREATE TABLE `user` (
  5. `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  6. `name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '员工姓名',
  7. `phone_num` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '联系方式',
  8. `address` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '住址',
  9. PRIMARY KEY (`id`)
  10. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

2. 准备一个用户信息导入的初始化模板;

image

3. 模拟创造百万数据量在User表中;

这一点其实有2种方案,第一种就是在创造好的模板文件xlsx中,手工造出100万的数据,xlsx单个sheet页最大可创建104万行数据,刚刚好满足,如果用xls单个sheet还不可以,这种肯定相对麻烦,并且100万的数据有几十M,打开就已经很慢了;

另外一种方案,可以通过存储过程向MySQL中加入100w条数据,不过性能也不好,毕竟数据量太大,自己斟酌吧,sql贴出来(性能不好的电脑,不建议这么干,容易把软件跑崩):

  1. DELIMITER //
  2. drop procedure IF EXISTS InsertTestData;
  3. CREATE PROCEDURE InsertTestData()
  4. BEGIN
  5. DECLARE counter INT DEFAULT 1;
  6. WHILE counter < 1000000 DO
  7. INSERT INTO user (id, name, phone_num, address) VALUES
  8. (counter, CONCAT('name_', counter), CONCAT('phone_', counter), CONCAT('add_',counter)) ;
  9. SET counter = counter + 1;
  10. END WHILE;
  11. END //
  12. DELIMITER;
  13. -- 调用存储过程插入数据
  14. CALL InsertTestData();

五、SpringBoot中配置EasyExcel

5.1 pom.xml中引入依赖

本次代码中一共用到了如下这些依赖,很多小伙伴本地若已经引入了,可以忽略!

  1. <!--lombok依赖-->
  2. <dependency>
  3. <groupId>org.projectlombok</groupId>
  4. <artifactId>lombok</artifactId>
  5. <optional>true</optional>
  6. </dependency>
  7. <!--MyBatis Plus依赖-->
  8. <dependency>
  9. <groupId>com.baomidou</groupId>
  10. <artifactId>mybatis-plus-boot-starter</artifactId>
  11. <version>3.4.0</version>
  12. </dependency>
  13. <!--easyexcel-->
  14. <dependency>
  15. <groupId>com.alibaba</groupId>
  16. <artifactId>easyexcel</artifactId>
  17. <version>3.3.4</version>
  18. </dependency>
  19. <!-- hutool -->
  20. <dependency>
  21. <groupId>cn.hutool</groupId>
  22. <artifactId>hutool-all</artifactId>
  23. <version>5.8.25</version>
  24. </dependency>

5.2 创建实体类

  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. @ColumnWidth(25)
  5. public class User {
  6. /**
  7. * 主键
  8. *
  9. * @mbg.generated
  10. */
  11. @ExcelProperty("id")
  12. private Integer id;
  13. /**
  14. * 员工姓名
  15. *
  16. * @mbg.generated
  17. */
  18. @ExcelProperty("姓名")
  19. private String name;
  20. /**
  21. * 联系方式
  22. *
  23. * @mbg.generated
  24. */
  25. @ExcelProperty("联系方式")
  26. private String phoneNum;
  27. /**
  28. * 住址
  29. *
  30. * @mbg.generated
  31. */
  32. @ExcelProperty("联系地址")
  33. private String address;
  34. }

【注解说明】

  • @ExcelProperty:声明列名。
  • @ColumnWidth:设置列宽。也可以直接作用在类上。统一每一列的宽度

5.3 创建数据关系映射

UserMapper 文件

  1. //*注:这里面继承了mybatis-plus的BaseMapper接口,供后面进行分页查询使用。*
  2. public interface UserMapper extends BaseMapper<User> {
  3. int deleteByPrimaryKey(Integer id);
  4. int insertAll(User record);
  5. void insertSelective(@Param("list") List<User> list);
  6. User selectByPrimaryKey(Integer id);
  7. int updateByPrimaryKeySelective(User record);
  8. int updateByPrimaryKey(User record);
  9. Integer countNum();
  10. }

UserMapper .xml文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="org.javaboy.vhr.mapper.UserMapper">
  4. <resultMap id="BaseResultMap" type="org.javaboy.vhr.pojo.User">
  5. <id column="id" jdbcType="INTEGER" property="id" />
  6. <result column="name" jdbcType="VARCHAR" property="name" />
  7. <result column="phone_num" jdbcType="VARCHAR" property="phoneNum" />
  8. <result column="address" jdbcType="VARCHAR" property="address" />
  9. </resultMap>
  10. <sql id="Base_Column_List">
  11. id, name, phone_num, address
  12. </sql>
  13. <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
  14. select
  15. <include refid="Base_Column_List" />
  16. from user
  17. where id = #{id,jdbcType=INTEGER}
  18. </select>
  19. <select id="countNum" resultType="java.lang.Integer">
  20. select count(*) from user
  21. </select>
  22. <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
  23. delete from user
  24. where id = #{id,jdbcType=INTEGER}
  25. </delete>
  26. <insert id="insertAll" keyColumn="id" keyProperty="id" parameterType="org.javaboy.vhr.pojo.User" useGeneratedKeys="true">
  27. insert into user (name, phone_num, address
  28. )
  29. values (#{name,jdbcType=VARCHAR}, #{phoneNum,jdbcType=VARCHAR}, #{address,jdbcType=VARCHAR}
  30. )
  31. </insert>
  32. <insert id="insertSelective" parameterType="org.javaboy.vhr.pojo.User">
  33. insert into user
  34. (id,name, phone_num, address
  35. )
  36. values
  37. <foreach collection="list" item="item" separator=",">
  38. (#{item.id},#{item.name},#{item.phoneNum},#{item.address})
  39. </foreach>
  40. </insert>
  41. <update id="updateByPrimaryKeySelective" parameterType="org.javaboy.vhr.pojo.User">
  42. update user
  43. <set>
  44. <if test="name != null">
  45. name = #{name,jdbcType=VARCHAR},
  46. </if>
  47. <if test="phoneNum != null">
  48. phone_num = #{phoneNum,jdbcType=VARCHAR},
  49. </if>
  50. <if test="address != null">
  51. address = #{address,jdbcType=VARCHAR},
  52. </if>
  53. </set>
  54. where id = #{id,jdbcType=INTEGER}
  55. </update>
  56. <update id="updateByPrimaryKey" parameterType="org.javaboy.vhr.pojo.User">
  57. update user
  58. set name = #{name,jdbcType=VARCHAR},
  59. phone_num = #{phoneNum,jdbcType=VARCHAR},
  60. address = #{address,jdbcType=VARCHAR}
  61. where id = #{id,jdbcType=INTEGER}
  62. </update>
  63. </mapper>

六、前端设计

前端页面采用Vue框架实现,咱们就按照上文中构想的那三点来设计就行,可以简单点实现,如果想要更加炫酷的前端样式,比如导入的文件格式校验,数据量提示等等,可以自行网上学习哈。

  1. <template>
  2. <el-card>
  3. <div>
  4. <!--导入数据-->
  5. <el-upload
  6. :show-file-list="false"
  7. :before-upload="beforeUpload"
  8. :on-success="onSuccess"
  9. :on-error="onError"
  10. :disabled="importDataDisabled"
  11. style="display: inline-flex;margin-right: 8px"
  12. action="/employee/excel/import">
  13. <!--导入数据-->
  14. <el-button :disabled="importDataDisabled" type="success" :icon="importDataBtnIcon">
  15. {{importDataBtnText}}
  16. </el-button>
  17. </el-upload>
  18. <el-button type="success" @click="exportEasyExcel" icon="el-icon-download">
  19. 导出数据
  20. </el-button>
  21. <el-button type="success" @click="exportExcelTemplate" icon="el-icon-download">
  22. 导出模板
  23. </el-button>
  24. </div>
  25. </el-card>
  26. </template>
  27. <script>
  28. import {Message} from 'element-ui';
  29. export default {
  30. name: "Export",
  31. data() {
  32. return {
  33. importDataBtnText: '导入数据',
  34. importDataBtnIcon: 'el-icon-upload2',
  35. importDataDisabled: false,
  36. }
  37. },
  38. methods: {
  39. onError(res) {
  40. this.importDataBtnText = '导入数据';
  41. this.importDataBtnIcon = 'el-icon-upload2';
  42. this.importDataDisabled = false;
  43. console.log(res);
  44. },
  45. onSuccess(res) {
  46. this.importDataBtnText = '导入数据';
  47. this.importDataBtnIcon = 'el-icon-upload2';
  48. this.importDataDisabled = false;
  49. console.log(res.msg);
  50. if (res.msg == '文件导入成功'){
  51. Message.success("文件导入完成")
  52. }
  53. // this.initEmps();
  54. },
  55. beforeUpload() {
  56. this.importDataBtnText = '正在导入';
  57. this.importDataBtnIcon = 'el-icon-loading';
  58. this.importDataDisabled = true;
  59. },
  60. exportEasyExcel() {
  61. window.open('/employee/excel/easyexcelexport', '_parent');
  62. },
  63. exportExcelTemplate(){
  64. window.open('/employee/excel/exporttemplate', '_parent');
  65. }
  66. }
  67. }
  68. </script>
  69. <style scoped>
  70. </style>

效果如下:
image


七、导入导出实现

7.1 模板下载

1?? 将准备好的用户信息模板.xlsx文件放入resource对应路径下。

image

2?? 构建一个控制器类,用以接收导出模板、导入数据、导出数据的请求。

  1. @RestController
  2. @RequestMapping("/employee/excel")
  3. @AllArgsConstructor
  4. @Slf4j
  5. public class EasyExcellController {
  6. /**
  7. * 下载用户信息模板
  8. * @param response
  9. */
  10. @RequestMapping("/exporttemplate")
  11. public void downloadTemplate(HttpServletResponse response){
  12. try {
  13. //设置文件名
  14. InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/用户信息模板.xlsx");
  15. //设置头文件,注意文件名若为中文,使用encode进行处理
  16. response.setHeader("Content-disposition", "attachment;fileName=" + java.net.URLEncoder.encode("用户信息模板.xlsx", "UTF-8"));
  17. //设置文件传输类型与编码
  18. response.setContentType("application/vnd.ms-excel;charset=UTF-8");
  19. OutputStream outputStream = response.getOutputStream();
  20. byte[] bytes = new byte[2048];
  21. int len;
  22. while((len = inputStream.read(bytes)) != -1){
  23. outputStream.write(bytes,0,len);
  24. }
  25. outputStream.flush();
  26. outputStream.close();
  27. inputStream.close();
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }

这部分代码中需要注意的是,如果你的模板是中文名字,需要加上java.net.URLEncoder.encode("用户信息模板.xlsx", "UTF-8")解决乱码问题。

7.2 导入数据

1?? 在EasyExcellController类中增加导入数据的请求处理方法;

  1. @Autowired
  2. EasyExcelServiceImpl easyExcel;
  3. /**
  4. * 导入百万excel文件
  5. * @param file
  6. * @return
  7. */
  8. @RequestMapping("/import")
  9. public RespBean easyExcelImport(MultipartFile file){
  10. if(file.isEmpty()){
  11. return RespBean.error("文件不可为空");
  12. }
  13. easyExcel.easyExcelImport(file);
  14. return RespBean.ok("文件导入成功");
  15. }

代码中的RespBean是自己定义的一个响应工具类。

  1. public class RespBean {
  2. private Integer status;
  3. private String msg;
  4. private Object obj;
  5. public static RespBean build() {
  6. return new RespBean();
  7. }
  8. public static RespBean ok(String msg) {
  9. return new RespBean(200, msg, null);
  10. }
  11. public static RespBean ok(String msg, Object obj) {
  12. return new RespBean(200, msg, obj);
  13. }
  14. public static RespBean error(String msg) {
  15. return new RespBean(500, msg, null);
  16. }
  17. public static RespBean error(String msg, Object obj) {
  18. return new RespBean(500, msg, obj);
  19. }
  20. private RespBean() {
  21. }
  22. private RespBean(Integer status, String msg, Object obj) {
  23. this.status = status;
  24. this.msg = msg;
  25. this.obj = obj;
  26. }
  27. public Integer getStatus() {
  28. return status;
  29. }
  30. public RespBean setStatus(Integer status) {
  31. this.status = status;
  32. return this;
  33. }
  34. public String getMsg() {
  35. return msg;
  36. }
  37. public RespBean setMsg(String msg) {
  38. this.msg = msg;
  39. return this;
  40. }
  41. public Object getObj() {
  42. return obj;
  43. }
  44. public RespBean setObj(Object obj) {
  45. this.obj = obj;
  46. return this;
  47. }
  48. }

2?? 在控制器中引入的easyExcel.easyExcelImport(file)方法中进行导入逻辑的实现。

  1. @Service
  2. @Slf4j
  3. @AllArgsConstructor
  4. public class EasyExcelServiceImpl implements EasyExcelService {
  5. private final ApplicationContext applicationContext;
  6. /**
  7. * excle文件导入实现
  8. * @param file
  9. */
  10. @Override
  11. public void easyExcelImport(MultipartFile file) {
  12. try {
  13. long beginTime = System.currentTimeMillis();
  14. //加载文件读取监听器
  15. EasyExcelImportHandler listener = applicationContext.getBean(EasyExcelImportHandler.class);
  16. //easyexcel的read方法进行数据读取
  17. EasyExcel.read(file.getInputStream(), User.class,listener).sheet().doRead();
  18. log.info("读取文件耗时:{}秒",(System.currentTimeMillis() - beginTime)/1000);
  19. } catch (IOException e) {
  20. log.error("导入异常", e.getMessage(), e);
  21. }
  22. }
  23. }

这部分代码的核心是文件读取监听器:EasyExcelImportHandler。

3?? 构建文件读取监听器

  1. @Slf4j
  2. @Service
  3. public class EasyExcelImportHandler implements ReadListener<User> {
  4. /*成功数据*/
  5. private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>();
  6. /*单次处理条数*/
  7. private final static int BATCH_COUNT = 20000;
  8. @Resource
  9. private ThreadPoolExecutor threadPoolExecutor;
  10. @Resource
  11. private UserMapper userMapper;
  12. @Override
  13. public void invoke(User user, AnalysisContext analysisContext) {
  14. if(StringUtils.isNotBlank(user.getName())){
  15. successList.add(user);
  16. return;
  17. }
  18. if(successList.size() >= BATCH_COUNT){
  19. log.info("读取数据:{}", successList.size());
  20. saveData();
  21. }
  22. }
  23. /**
  24. * 采用多线程读取数据
  25. */
  26. private void saveData() {
  27. List<List<User>> lists = ListUtil.split(successList, 20000);
  28. CountDownLatch countDownLatch = new CountDownLatch(lists.size());
  29. for (List<User> list : lists) {
  30. threadPoolExecutor.execute(()->{
  31. try {
  32. userMapper.insertSelective(list.stream().map(o -> {
  33. User user = new User();
  34. user.setName(o.getName());
  35. user.setId(o.getId());
  36. user.setPhoneNum(o.getPhoneNum());
  37. user.setAddress(o.getAddress());
  38. return user;
  39. }).collect(Collectors.toList()));
  40. } catch (Exception e) {
  41. log.error("启动线程失败,e:{}", e.getMessage(), e);
  42. } finally {
  43. //执行完一个线程减1,直到执行完
  44. countDownLatch.countDown();
  45. }
  46. });
  47. }
  48. // 等待所有线程执行完
  49. try {
  50. countDownLatch.await();
  51. } catch (Exception e) {
  52. log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e);
  53. }
  54. // 提前将不再使用的集合清空,释放资源
  55. successList.clear();
  56. lists.clear();
  57. }
  58. /**
  59. * 所有数据读取完成之后调用
  60. * @param analysisContext
  61. */
  62. @Override
  63. public void doAfterAllAnalysed(AnalysisContext analysisContext) {
  64. //读取剩余数据
  65. if(CollectionUtils.isNotEmpty(successList)){
  66. log.info("读取数据:{}条",successList.size());
  67. saveData();
  68. }
  69. }
  70. }

在这部分代码中我们需要注意两个问题,第一个是多线程,第二个是EasyExcel提供的ReadListener监听器。

第一个,由于我们在代码里采用了多线程导入,因此我们需要配置一个合理的线程池,以提高导入效率。

  1. @Configuration
  2. public class EasyExcelThreadPoolExecutor {
  3. @Bean(name = "threadPoolExecutor")
  4. public ThreadPoolExecutor easyExcelStudentImportThreadPool() {
  5. // 系统可用处理器的虚拟机数量
  6. int processors = Runtime.getRuntime().availableProcessors();
  7. return new ThreadPoolExecutor(processors + 1,
  8. processors * 2 + 1,
  9. 10 * 60,
  10. TimeUnit.SECONDS,
  11. new LinkedBlockingQueue<>(1000000));
  12. }
  13. }

第二个,对于ReadListener,我们需要搞清楚它提供的方法的作用。

  • invoke():读取表格内容,每一条数据解析都会来调用;
  • doAfterAllAnalysed():所有数据解析完成了调用;
  • invokeHead() :读取标题,里面实现在读完标题后会回调,本篇文章中未使用到;
  • onException():转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行,本篇文章中未使用到。

4?? 导入100万数据量耗时测试

在做导入测试前,由于100万数据量的excel文件很大,所以我们要在application.yml文件中进行最大可上传文件的配置:

  1. spring:
  2. servlet:
  3. multipart:
  4. max-file-size: 128MB # 设置单个文件最大大小为10MB
  5. max-request-size: 128MB # 设置多个文件大小为100MB

对100万数据进行多次导入测试,所损耗时间大概在500秒左右,8分多钟,这对于我们来说肯定无法接受,所以我们在后面针对这种导入进行彻底优化!

image

7.3 导出数据

1?? 在EasyExcellController类中增加导出数据的请求处理方法;

  1. /**
  2. * 导出百万excel文件
  3. * @param response
  4. */
  5. @RequestMapping("/easyexcelexport")
  6. public void easyExcelExport(HttpServletResponse response){
  7. try {
  8. //设置内容类型
  9. response.setContentType("text/csv");
  10. //设置响应编码
  11. response.setCharacterEncoding("utf-8");
  12. //设置文件名的编码格式,防止文件名乱码
  13. String fileName = URLEncoder.encode("用户信息", "UTF-8");
  14. //固定写法,设置响应头
  15. response.setHeader("Content-disposition", "attachment;filename="+ fileName + ".xlsx");
  16. Integer total = userMapper.countNum();
  17. if (total == 0) {
  18. log.info("查询无数据");
  19. return;
  20. }
  21. //指定用哪个class进行写出
  22. ExcelWriter build = EasyExcel.write(response.getOutputStream(), User.class).build();
  23. //设置一个sheet页存储所有导出数据
  24. WriteSheet writeSheet = EasyExcel.writerSheet("sheet").build();
  25. long pageSize = 10000;
  26. long pages = total / pageSize;
  27. long startTime = System.currentTimeMillis();
  28. //数据量只有一页时直接写出
  29. if(pages < 1){
  30. List<User> users = userMapper.selectList(null);
  31. build.write(users, writeSheet);
  32. }
  33. //大数据量时,进行分页查询写入
  34. for (int i = 0; i <= pages; i++) {
  35. Page<User> page = new Page<>();
  36. page.setCurrent(i + 1);
  37. page.setSize(pageSize);
  38. Page<User> userPage = userMapper.selectPage(page, null);
  39. build.write(userPage.getRecords(), writeSheet);
  40. }
  41. build.finish();
  42. log.info("导出耗时/ms:"+(System.currentTimeMillis()-startTime)+",导出数据总条数:"+total);
  43. } catch (Exception e) {
  44. log.error("easyExcel导出失败,e:{}",e.getMessage(),e);
  45. }
  46. }

由于数据量比较大,我们在这里采用分页查询,写入到一个sheet中,如果导出到xls格式的文件中,需要写入到多个sheet中,这种可能会慢一点。

且在Mybatis-Plus中使用分页的话,需要增加一个分页插件的配置

  1. @Configuration
  2. public class MybatisPlusPageConfig {
  3. /**
  4. * 新版分页插件配置
  5. */
  6. @Bean
  7. public MybatisPlusInterceptor mybatisPlusInterceptor() {
  8. MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
  9. mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
  10. return mybatisPlusInterceptor;
  11. }
  12. }

2?? 百万数据量导出测试

经过多次测试发现,100万数据量平均导出耗时在40秒左右,在可以接受的范围内!

image

八、总结

以上就是SpringBoot项目下,通过阿里开源的EasyExcel技术进行百万级数据的导入与导出,不过针对百万数据量的导入,时间在分钟级别,这很明显不够优秀,但考虑到本文的篇幅已经很长了,我们在下一篇文章针对导入进行性能优化,敬请期待!

九、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

image

原文链接:https://www.cnblogs.com/JavaBuild/p/18185854

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号