高质量实现单文件导入、导出功能(使用EasyExcel )

news2024/9/9 5:23:52

前言

咋说呢,最近交接了一个XXX统计分析系统到我手上,显示页面平平无其,一看导入、导出功能的实现代码,每个小菜单目录里面都对应一个导入导出接口,看起来十分难受!(此处省略1w字内心os)正所谓前人栽树、后人乘凉,bug也是接踵而来,打了我个措手不及呀!于是想着去优化一波代码,故事的正文由此展开

解耦

解耦真的很重要!重要的事情说三遍!好的代码规范结合良好的架构,让项目日后的运营和维护都变得容易了许多,最重要的是自己看着也舒服

如何将所有的导入接口合并成一个接口?

要想实现如何将所有的导入接口合并成一个接口前,首先我们要弄清楚这些导入接口间有哪几块是可以共用的,哪几部分又是各自差异化的,下面我来简单罗列一下

共用

  1. 解析导入的文件这个流程是一样的,都是先读取 execel 流,然后逐行解析数据
  2. 接口实现的逻辑这个流程是一样的,都是先解析数据,然后过滤数据,最后对数据进行入库

差异化

  1. 每个导入接口,入库的时候用到的 service 是不一样的
  2. 每个导入接口,过滤数据的逻辑是不一样的、入库的逻辑也可能不一样,举个例子来说:可能有些需要先删除旧数据,然后插入解析到的数据。有一些是先入库后删除旧数据。

搞清楚了这几点之后,我们就可以从代码角度去扩展实现原有的功能了

导入功能、抽离同质化的逻辑实现

java 中有一个东西叫做 抽象类 ,使用抽象类即可将:差异化的东西抽象出来,同质化的东西进行复用。
以下代码为我多次调试、以及测试验证后的最终版代码,大体的逻辑就是利用 Easy Excel 中的 ExcelReader 来对文件进行解析,解析完成后,我们对数据进行一个入库操作,当然其中也实现了一些个性化操作,考虑到上文提到的差异化的部分,提供了一些扩展点给开发人员的,对数据过滤、最终数据入库要怎么去实现交给子类去实现

@Slf4j
public abstract class AbstractAnalysisEventListener<T> extends AnalysisEventListener<T> {
    /**
     * 应对多线程下的导入问题:每个线程解析到的导入数据都与其线程挂钩,达到副本独立的效果
     */
    private ThreadLocal<List<T>> datas = new ThreadLocal<>();
    private ImportVo importVo;
    private final String defaultSheetName = "Sheet1";
    private final Integer defaultSheetNo = 1;
    private final Integer defaultHeadRowNumber = 1;

    @Override
    public void invoke(T source, AnalysisContext analysisContext) {
        if (!whetherImport(source, importVo)) return;
        if (null == datas.get()) datas.set(new ArrayList<>());
        datas.get().add(formatData(source, importVo));
    }

    /**
     * 文件解析完毕
     */
    @Transactional
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("开始准备数据入库!");
        try {
            insertAllDate(datas.get());
            log.info("数据导入完成开始 jc");
            jc();
            log.info("jc 结束");
        } catch (Exception e) {
            e.printStackTrace();
            log.error("数据入库过程中出现异常:" + e.getMessage());
            throw e;
        }
        log.info("数据入库完成!");
    }

    public void doExecute(ImportVo importVo, MultipartFile excelFile) throws InterruptedException {
        ExcelReader reader = null;
        this.importVo = importVo;
        try {
            reader = getReader(excelFile, this);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("初始化 ExcelReader 出现异常: " + e.getMessage() + " fileName:" + excelFile.getOriginalFilename());
            throw new RuntimeException("初始化 ExcelReader 出现异常");
        }
        log.info("开始解析excel文件!");
        ArrayList<ReadSheet> readSheets = new ArrayList<>();
        ReadSheet readSheet = new ReadSheet();
        if (null == importVo.getHeadRowNumber()) readSheet.setHeadRowNumber(defaultHeadRowNumber);
        else readSheet.setHeadRowNumber(importVo.getHeadRowNumber());
        readSheet.setClazz(aClass());
        if (null == importVo.getSheetNo()) readSheet.setSheetNo(defaultSheetNo);
        else readSheet.setSheetNo(importVo.getSheetNo());
        if (StringUtils.isEmpty(importVo.getSheetName())) readSheet.setSheetName(defaultSheetName);
        else readSheet.setSheetName(importVo.getSheetName());
        readSheets.add(readSheet);
        reader.read(readSheets);
        log.info("excel文件解析完成!");
        // 弃用的方法
        //reader.read(new Sheet(sheetNo, headLineNum, rowModel));
    }

    private ExcelReader getReader(MultipartFile excel, AnalysisEventListener excelListener) throws IOException {
        String filename = excel.getOriginalFilename();
        if (filename != null && (filename.toLowerCase().endsWith(".xls") || filename.toLowerCase().endsWith(".xlsx"))) {
            InputStream is = new BufferedInputStream(excel.getInputStream());
            ArrayList<ReadListener> readListeners = new ArrayList<>();
            readListeners.add(excelListener);
            ReadWorkbook readWorkbook = new ReadWorkbook();
            readWorkbook.setInputStream(is);
            readWorkbook.setCustomReadListenerList(readListeners);
            ExcelReader excelReader = new ExcelReader(readWorkbook);
            return excelReader;
            //弃用的方法
            //return new ExcelReader(is, null, excelListener, false);
        } else {
            log.error("文件格式错误、文件名不能为空");
            throw new RuntimeException("文件格式错误、文件名不能为空");
        }
    }

    public void jc() {
        importVo = null;
        datas.remove();
    }

    public abstract Boolean whetherImport(T t, ImportVo importVo);

    public abstract void insertAllDate(List<T> datas);

    public abstract T formatData(T source, ImportVo importVo);

    public abstract String type();

    public abstract Class aClass();
}

代码设计精髓 使用Threadlocal 副本隔离

好了代码就是长上面这样了,现在来说一下代码的思想吧,可能之前积累了一些看源码的经验,竟感觉经历都是如此的类似。对 Threadlocal 不了解的可以移步 ThreadLocal还存在内存泄漏?源码级别解读

ThreadLocal 在源码中最经典的应用场景莫过于: Spring 对 Jdbc 事务的封装了,事务对象存放在 ThreadLocal 中(图二),下面来举个例子(图一),m3出现异常由于m1开启了事务,m1,m2,m3都会进行回滚,但是有一个前提就是,m1,m2,m3 使用的是同一个连接,因为回滚是根据连接来的,由于连接与线程进行了一一绑定,很容易实现 m1,m2,m3 都是用同一个连接的逻辑,因为都是在一个线程中执行(子事务的运行机制)。其次由于线程与连接绑定了,也不会出现连接污染的情况出现,一个线程中的数据回滚了,不会影响另一个线程中的数据。当然这些逻辑的实现也可以不使用 ThreadLocal ,使用一个 Map 也可以,不过需要编写一大堆的判断逻辑,还极容易出错!

图一
在这里插入图片描述
图二
在这里插入图片描述
回到我们实现的 AbstractAnalysisEventListener 中来,由于之后的各种导入只需实现 AbstractAnalysisEventListener 类,重写一下 type()、aClass()方法就好了,各种导入的文件在对其解析的时候,数据都会存放到 datas 中,单个线程下是没有任何问题的,但是在多个线程下,datas 中的数据就会出现各种问题,比方说,线程一导入的数据解析并且入库完成了,开始清除 datas 中的数据,但是线程二的数据还在解析中,此时的 datas 由于被清空了,就会造成线程二,导入数据条数缺失的问题,等等,所以此处使用 ThreadLocal 对数据进行一个线程隔离,每个线程的数据互不影响。注:一个线程内导入多个文件还是不支持的

解析器的实现

支持的功能:判决每一条数据是否需要入库、格式化每一条需要导入的数据、导入所有符合规则的数据,详情见代码中注释

@Data
@Component
public class GoodsImportAnalysis extends AbstractAnalysisEventListener<Goods> {
    @Autowired
    private GoodsService goodsService;

    /**
     * @param goods    解析到的每一条数据
     * @param importVo 前端传过来的参数
     * @return true: 这条数据需要入库、false: 这条数据无需入库
     */
    @Override
    public Boolean whetherImport(Goods goods, ImportVo importVo) {
        return true;
    }

    /**
     * 其实这个方法也可以直接写在抽象类中,提供出来的目的在于:
     * 如果日后有需求说什么,导入这个数据的同时还要删掉其他别的数据,或者先删后入库等等其他需求,提供扩展点给用户自己选择
     * @param datas 经过 whetherImport 、 formatData 方法筛选最终解析得到的所有数据
     */
    @Override
    public void insertAllDate(List<Goods> datas) {
        goodsService.saveBatch(datas);
    }


    /**
     * @param goods    解析到的每一条数据
     * @param importVo 前端传过来的参数
     * @return 格式化过后的一条数据
     */
    @Override
    public Goods formatData(Goods goods, ImportVo importVo) {
        goods.setType(Thread.currentThread().getName());
        return goods;
    }

    /**
     * 根据 type()的返回值找到对应的解析器解析文件
     */
    @Override
    public String type() {
        return "GoodsImportAnalysis";
    }

    /**
     * 提供解析类给予参照
     */
    @Override
    public Class aClass() {
        return Goods.class;
    }

}

导入功能统一 Service

通过实现 ApplicationContextAware 接口,获取 Spring 上下文来获取所有 AbstractAnalysisEventListener 类型的 Bean ,再根据前端传过来的参数 type 确定要用哪一个解析器来进行处理,这样一来:所有的导入接口就都统一成一个接口了。

@Slf4j
@Component
public class ImportService implements ApplicationContextAware, DisposableBean {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void destroy() {
        this.applicationContext = null;
    }

    public void execute(ImportVo importVo, MultipartFile multipartFile) {
        Map<String, AbstractAnalysisEventListener> beansOfType = applicationContext.getBeansOfType(AbstractAnalysisEventListener.class);
        if(beansOfType.size()==0) throw new RuntimeException("系统未提供适配器!");
        boolean res = beansOfType.entrySet().stream().anyMatch(entry -> {
            AbstractAnalysisEventListener abstractAnalysisEventListener = entry.getValue();
            if (importVo.getType().equals(abstractAnalysisEventListener.type())) {
                try {
                    abstractAnalysisEventListener.doExecute(importVo, multipartFile);
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("导入文件时出现异常:" + e.getCause());
                    throw new RuntimeException(e);
                }
            }
            return false;
        });
        if (!res) throw new RuntimeException("未找到合适的适配器执行!");
    }
}

导入测试

goods导入模版
在这里插入图片描述
goods导入模版2
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
如果需要导入别的数据只需编写对应的 AbstractAnalysisEventListener 实现类,接口还是用如下这个接口,就可以了,如果日后解析 execel 这一步骤需要换成 poi 技术实现的话,我们只需修改 AbstractAnalysisEventListener 里面的逻辑就可以了,是不是很方便呢

   @PostMapping(value = "import")
    public R import2(@RequestParam("file") MultipartFile file,
                     @RequestParam("file2") MultipartFile file2,
                     @RequestPart ImportVo customParams) {
        new Thread(() -> {
            importService.execute(customParams, file);
        }, "用户一").start();
        new Thread(() -> {
            importService.execute(customParams, file2);
        }, "用户二").start();
        return R.ok("success");
    }

~~~~~~~~~~~~到此和导入有关的内容结束~~~~·~~~~~~~~

如何将所有的导出接口合并成一个接口?

老样子抽象、封装,对 EasyExcel 中的方法进行封装,支持了指定表头的数据导出,只不过动态表头是交与前端来传入的,后端解析一下就好了。里面值得注意的是,使用到了反射技术将表头与数据进行对齐

@Slf4j
public abstract class AbstractExportService implements ExecelHander {
    public void execute(HttpServletResponse response, ExportVo exportVo) {
        this.writeExcel(response,
                dataList(list(exportVo), exportVo.getColumns().values()),
                exportVo.getFileName(),
                exportVo.getSheetName(),
                aClass(),
                head(exportVo.getColumns().keySet()));
    }

    /**
     * 使用 linkedlist 将 db 中查出来的每一条数据字段值顺序,与前端表头传参保持一致,返回值里层list对应一行数据,外层list对应多行数据
     */
    private List<List<String>> head(Set<String> list) {
        List<List<String>> lists = new ArrayList<>();
        list.stream().forEach(l -> {
            ArrayList<String> strings = new ArrayList<>();
            strings.add(l);
            lists.add(strings);
        });
        return lists;
    }

    /**
     * 使用 linkedlist 将 db 中查出来的每一条数据字段值顺序,与前端表头传参保持一致
     */
 	private List<LinkedList> dataList(List dblist, Collection<String> fileList) {
        LinkedList list = new LinkedList<LinkedList>();
        for (Object goods : dblist) {
            List data = new LinkedList<>();
            fileList.stream().forEach(heads -> {
                data.add(getFieldValue(String.valueOf(heads), goods));
            });
            list.add(data);
        }
        return list;
    }

    /**
     * 通过反射获取对应的值
     */
    private static Object getFieldValue(String fieldName, Object person) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = person.getClass().getMethod(getter);
            return method.invoke(person);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("使用反射获取对象属性值失败", e);
            return "使用反射获取对象属性值失败,请检查参数 columns:[" + fieldName + "] 是否正确";
        }
    }

    public abstract String type();

    public abstract List list(ExportVo exportVo);

    public abstract Class aClass();
}

对应的 ExecelHander,里面主要还是传统的导出代码,没啥好看的

public interface ExecelHander {
    /**
     * 导出单个sheet
     * @param data      导出数据List
     * @param fileName  文件名
     * @param sheetName sheet名
     * @param model     映射实体
     */
    default void writeExcel(HttpServletResponse response, List<? extends Object> data, String fileName, String sheetName,
                            Class model, List<List<String>> heads) {
        try {
            // 表头样式策略
            WriteCellStyle headWriteCellStyle = new WriteCellStyle();
            // 设置数据格式
            headWriteCellStyle.setDataFormat((short) BuiltinFormats.getBuiltinFormat("m/d/yy h:mm"));
            // 是否换行
            headWriteCellStyle.setWrapped(false);
            // 水平对齐方式
            headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
            // 垂直对齐方式
            headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
            // 前景色
            headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
            // 背景色
            headWriteCellStyle.setFillBackgroundColor(IndexedColors.WHITE.getIndex());
            // 设置为1时,单元格将被前景色填充
            headWriteCellStyle.setFillPatternType(FillPatternType.NO_FILL);
            // 控制单元格是否应自动调整大小以适应文本过长时的大小
            headWriteCellStyle.setShrinkToFit(false);
            // 单元格边框类型
            headWriteCellStyle.setBorderBottom(BorderStyle.NONE);
            headWriteCellStyle.setBorderLeft(BorderStyle.NONE);
            headWriteCellStyle.setBorderRight(BorderStyle.NONE);
            headWriteCellStyle.setBorderTop(BorderStyle.NONE);
            // 单元格边框颜色
            headWriteCellStyle.setLeftBorderColor(IndexedColors.BLACK.index);
            headWriteCellStyle.setRightBorderColor(IndexedColors.BLACK.index);
            headWriteCellStyle.setTopBorderColor(IndexedColors.BLACK.index);
            headWriteCellStyle.setBottomBorderColor(IndexedColors.BLACK.index);
            // 字体策略
            WriteFont writeFont = new WriteFont();
            writeFont.setBold(false);
            // 字体颜色
            writeFont.setColor(Font.COLOR_NORMAL);
            // 字体名称
            writeFont.setFontName("宋体");
            // 字体大小
            writeFont.setFontHeightInPoints((short) 11);
            // 是否使用斜体
            writeFont.setItalic(false);
            // 是否在文本中使用横线删除
            writeFont.setStrikeout(false);
            // 设置要使用的文本下划线的类型
            writeFont.setUnderline(Font.U_NONE);
            // 设置要使用的字符集
            writeFont.setCharset(FontCharset.DEFAULT.getNativeId());
            headWriteCellStyle.setWriteFont(writeFont);

            // 内容样式策略策略
            WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
            contentWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
            contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.GENERAL);
            contentWriteCellStyle.setBorderBottom(BorderStyle.NONE);
            contentWriteCellStyle.setBorderLeft(BorderStyle.NONE);
            contentWriteCellStyle.setBorderRight(BorderStyle.NONE);
            contentWriteCellStyle.setBorderTop(BorderStyle.NONE);
            contentWriteCellStyle.setFillPatternType(FillPatternType.NO_FILL);
            contentWriteCellStyle.setWrapped(false);
            EasyExcel.write(getOutputStream(fileName, response), model)
                    .head(heads)
                    .registerWriteHandler(new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle))
                    .registerWriteHandler(new SimpleColumnWidthStyleStrategy(16))
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet(sheetName)
                    .doWrite(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    default OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
        try {
            fileName = URLEncoder.encode(fileName, "utf-8");
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".xlsx");
            response.setHeader("Pragma", "public");
            response.setHeader("Cache-Control", "no-store");
            response.addHeader("Cache-Control", "max-age=0");
            return response.getOutputStream();
        } catch (IOException e) {
            throw new Exception("导出excel表格失败!", e);
        }
    }
}

导出功能统一 Service

套路和导入一样,~不多bb

@Component
public class ExportService implements ApplicationContextAware, DisposableBean {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void destroy() {
        this.applicationContext = null;
    }

    public void execute(ExportVo exportVo, HttpServletResponse response) {
        Map<String, AbstractExportService> beansOfType = applicationContext.getBeansOfType(AbstractExportService.class);
        beansOfType.entrySet().stream().forEach(entry -> {
            AbstractExportService abstractExportService = entry.getValue();
            if (null != abstractExportService.type() && abstractExportService.type().equals(exportVo.getType())) {
                abstractExportService.execute(response,exportVo);
            }
        });
    }
}

Goods导出

只需实现一下我们的 AbstractExportService ,重写一下 list 方法就行了

@Component
public class GoodsExportService extends AbstractExportService {
    @Autowired
    private GoodsService goodsService;
    /**
     * @param exportVo 导出条件
     * @return 导出文件中的数据来源
     */
    @Override
    public List list(ExportVo exportVo) {
        List ids = (List) exportVo.getCustomParams().get("id");
        LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<Goods>()
                .in(Goods::getId, ids);
        List<Goods> list = goodsService.list(wrapper);
        return list;
    }

    @Override
    public Class aClass() {
        return Goods.class;
    }

    @Override
    public String type() {
        return "GoodsExportService";
    }
}

Student导出

@Component
public class StudentExportService extends AbstractExportService {
    @Autowired
    private StudentService studentService;

    @Override
    public String type() {
        return "StudentExportService";
    }

    @Override
    public List list(ExportVo exportVo) {
        List<Student> list = studentService.list();
        return list;
    }

    @Override
    public Class aClass() {
        return Student.class;
    }
}

导出效果

可以看到,实现了自定义表头列的功能,且顺序和传参的顺序一致,真不错,而且由于指定了导出条件,只导出id为1、2的数据
在这里插入图片描述
在这里插入图片描述

附页(导入文件解析成功、但是映射不上)

由于本文的基础代码是用 Mybatis Plus 生成的,对应的 Entity 类上面加了 @Accessors(chain = true) 这个注解,导致 Easy Excel 在做数据解析映射的时候,映射不上去,对应源码中 ModelBuildEventListener 类下面的 buildUserModel 方法,感兴趣的小伙伴可以在此处打一个断点,自行去 debug ,本文碍于篇幅不展开叙述了。解决办法去掉 @Accessors 。
在这里插入图片描述
~~~~~~~~~~~到此全文结束~~~~~~~~~~

题外话

如果读者追求极致的性能可以结合 ThreadPoolTaskExecutor 实现多线程批量导入,在对应入库逻辑里面改造即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/13629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Neo4j安装与配置

注意&#xff1a;在安装Neo4j之前要安装JAVA SE的SDK。 1. 下载Neo4j 企业版本的收费的&#xff0c;社区版本是免费的。我下载的是社区版本。 社区版本下载地址&#xff1a;https://neo4j.com/download-center/#community 我下载的是3.5.35 下载好以后是zip文件&#xff0c;…

vulnhub靶机raven1

靶机下载地址&#xff1a;Raven: 1 ~ VulnHub Kali ip&#xff1a;192.168.174.128 靶机ip&#xff1a;192.168.174.131 实验思路&#xff1a;靶机ip发现&#xff0c;端口扫描&#xff0c;wordpress用户枚举和爆破&#xff0c;ssh爆破&#xff0c;MySQL登录&#xff0c;pyth…

云计算存储虚拟化技术

存储虚拟化是指将具体的存储设备或存储系统与服务器操作系统分隔开&#xff0c;为存储用户提供一个统一的虚拟存储池。 在面对资源池按需分配、多租户、海量存储、高I/O、快速扩展、差异化服务等需求时&#xff0c;传统的存储方式面临的挑战日益明显&#xff0c;存储成本高、并…

kali(linux)安装fscan

fscan 是一个内网综合扫描工具&#xff0c;方便一键自动化、全方位漏洞扫描。 它支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写公钥、计划任务反弹shell、读取win网卡信息、web指纹识别、web漏洞扫描、netbios探测、域控识别等功能。 源码链接&#xff…

2022.11.17 HDU-4911 Inversion

bobo has a sequence a1,a2,…,an. He is allowed to swap two adjacent numbers for no more than k times. Find the minimum number of inversions after his swaps. Note: The number of inversions is the number of pair (i,j) where 1≤i<j≤n and ai>aj. Input T…

node篇 CommonJS规范

文章目录1. 每个文件就是一个模块&#xff0c;有自己的作用域。在一个文件里面定义的变量、函数、类&#xff0c;都是私有的&#xff0c;对其他文件不可见。2. 每个模块内部&#xff0c;module变量代表当前模块。3. 两种导出方式 exports 和 module.exports4. 一种引入方式 req…

腾讯云轻量应用服务器搭配WordPress程序使用宝塔面板一键式搭建个人博客网站

你想要拥有一个自己的网站吗&#xff0c;不在受第三方网站的约束&#xff0c;自由撰写文章帖子&#xff0c;随时随地发挥你的想象。其实搭建一个个人网站没有那么麻烦&#xff0c;完全小白用户也可以办到&#xff0c;只需要点点鼠标就可以完成&#xff0c;下面就详细介绍使用腾…

计网实验软件 Cisco Packet Tracer 8 获取并汉化

1. 注册账号 点击&#xff1a;https://skillsforall.com/course/getting-started-cisco-packet-tracer 进入 Skills for All 网站. 点击 Get Started&#xff0c;进入登录页 点击 Sign Up 注册&#xff0c;选择国家、出生年月&#xff0c;填写其他信息。 这里顺带放一下密码的…

HOOPS/QT集成指南

引言 以下部分&#xff0c;说明如何使用QtGUI工具包和HOOPS 3D应用程序框架构建应用程序。首先回顾编译/链接&#xff0c;然后讨论各个组件中的对象关系&#xff0c;然后概述正确创建和初始化对象以及启动Qt事件循环所需的编程步骤。 开发人员应该首先编译、链接和运行基本的q…

【11-导入人人generator项目并自动生成相关的文件商品子模块的调试公共模块common子模块的抽离与实现Lombok插件的安装】

一.知识回顾 【0.三高商城系统的专题专栏都帮你整理好了&#xff0c;请点击这里&#xff01;】 【1-系统架构演进过程】 【2-微服务系统架构需求】 【3-高性能、高并发、高可用的三高商城系统项目介绍】 【4-Linux云服务器上安装Docker】 【5-Docker安装部署MySQL和Redis服务】…

UDS知识整理(四):ECU复位——0x11服务

目录 一、0x11服务&#xff08;ECU复位&#xff09;简介 二、0x11服务信息格式 &#xff08;1&#xff09;请求格式 &#xff08;2&#xff09;正响应格式 &#xff08;3&#xff09;负响应格式 三、0x11服务举例 &#xff08;1&#xff09;如&#xff1a;硬件复位 &am…

戴尔PowerEdge R650服务器荣获国家级实验室5项证书

在今年的戴尔科技峰会上&#xff0c;IDC中国副总裁兼首席分析师武连峰的报告引起了广泛关注&#xff0c;他表示&#xff1a; “根据IDC对全球企业CEO调研&#xff0c;在CEO关注的重点中&#xff0c;技术依然排在第一位&#xff0c;然后是效率和增长。88%的全球CEO在2022年会维…

[附源码]java毕业设计龙虎时代健身房管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

知乎高赞:听说Rust要取代C++?

随着近几年Rust的大火&#xff0c;越来越多的人开始质疑&#xff0c;Rust势头这么猛&#xff0c;是不是要取代C?小慕认为&#xff0c;一门编程语言想要完全取代另一门编程语言是很难的&#xff0c;毕竟每一门技术都有它的专属优势和特点&#xff0c;总会在特别的地方发光发热。…

linux 不依赖xcode成功运行appium+ios自动化

一、原理 **1、【常规】**常规情况下&#xff0c;ios自动化需要mac机器&#xff0c;如果强行使用linux运行&#xff0c;过程会提示Xcodebuild Failure&#xff0c;也就是强依赖Xcode。 失败图如下&#xff1a; **2、【思路】**那有没有什么方法可以绕过xcode的限制呢&#xf…

创建SpringBoot项目的几种方式

使用 spring boot 提供的初始化器。 向导的方式&#xff0c;完成 spring boot 项目的创建&#xff1a; 使用 方便.(需要联网)就是下图这个,在pom.xml里 选择依赖,选择些单体SpringBoot需要用到的一些 Spring Web包含SpringMVC Tomcat SQL 基本的JDBC MysqlDriver Mybatis 第…

发电控制系统面临的主要安全威胁

电力行业 工控安全解决思路保障框架从电力行业对工控安全需求看&#xff0c;电力企业在主要是以合规性建设为主&#xff0c;在 2004 年原电监会 5 号令颁布开始&#xff0c;大部 分的电厂控制系统安全 建设已经按照 5 号令的要求进行了整改&#xff0c;形成“安全分区、网络专…

再手写线程池以及性能分析

文章目录前言一、为什么要用线程池线程池的作用线程池的使用场景二、线程池的构成以及相关API的实现线程池中的相关变量类型线程池的创建任务线程实现获取任务线程池的销毁线程池的使用前言 以前写的线程池文章请参考&#xff1a;线程池的简单实现 本次文章是对线程池的再次学…

使用Egg调用mysql实现增删改查接口操作

市场上数据库可视化工具很多&#xff0c;笔者更多时间使用的是 Navicat ,但是因为它是收费的&#xff0c;一些伙伴可能没发使用&#xff0c;所以笔者在这里推荐一款 工具 名为&#xff1a; DBevaer&#xff0c;它所展现出来的能力&#xff0c;与navicat 类似&#xff0c;并且还…

玩RTOS这么久,一问原子操作,蒙了~

已剪辑自: https://mp.weixin.qq.com/s/kvxcOHT-xHtMAjQqJu7Y2g [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3f9Rrei-1668695258073)(https://res.wx.qq.com/mmbizappmsg/zh_CN/htmledition/js/images/icon/audio/icon_qqmusic_source6201b5.sv…