• [技术干货] 使用 Java 和 FreeMarker 实现自动生成供货清单,动态生成 Word 文档,简化文档处理流程
    引言在电缆行业,生成供货清单是一项常见但繁琐的任务。本教程将介绍如何使用现代Java技术栈自动化这一过程,大幅提高工作效率和准确性。我们将使用SpringBoot作为框架,Apache POI处理Word文档,以及FreeMarker作为模板引擎来实现这一功能!让我们先了解一下这个问题的背景:在电缆行业,手动创建供货清单是一个复杂且重复的过程。这个过程不仅耗时,还容易出错,影响工作效率和数据准确性。为了解决这个问题,我们提出了一个技术方案,结合了以下几个关键技术:SpringBoot: 作为我们的主要开发框架Apache POI: 用于生成和操作Word文档FreeMarker模板引擎: 用于生成Word文件的内容这个方案的主要优势包括:灵活性: 使用FreeMarker模板可以轻松调整文档格式,而无需修改程序代码。效率: 自动化生成过程大大减少了人工操作,提高了办公效率。准确性: 自动化处理确保了数据的准确性和一致性。适用性: 特别适合电缆行业的业务需求,生成符合要求的.doc文件。通过阅读这篇博客,您将学习如何实现这个解决方案,从而帮助您或您的团队简化工作流程,提高生产效率。效果图: 项目结构src/├── main/│   ├── java/│   │   └── com/│   │       └── pw/│   │           ├── WordController.java  #负责生成测试数据并调用WordUtil工具类来生成Word文档│   │           └── utils/│   │               └── WordUtil.java  #这个工具类封装了使用FreeMarker生成Word文档的核心功能│   └── resources/│       └── templates/│           └── template.ftl #模版定义了Word文档的结构和样式,使用HTML和CSS来格式化内容1.WordController类:这个类是我们应用的入口点,负责生成测试数据并调用WordUtil来生成Word文档。2.WordUtil类:这个工具类封装了使用FreeMarker生成Word文档的核心逻辑。3.FreeMarker模版(template.ftl):这个模版定义了Word文档的结构和样式,使用HTML和CSS来格式化内容。源代码展示1.WordControllerimport com.pw.utils.WordUtil; import java.io.File;import java.io.IOException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; public class WordController {     public static void main(String[] args) throws IOException {        // 指定保存Word文件的目录        String filePath = "F:\\Poi2Word\\src\\main\\resources\\output"; // 更改为您希望的目录        new WordController().generateWordFile(filePath);    }     public void generateWordFile(String directory) throws IOException {        List<Map<String, Object>> listMap = new ArrayList<>();         //测试数据        addTestData(listMap, "4600025747", "绝缘导线", "AC10kV,JKLYJ,300", 1500, "米", "盘号:A1");        addTestData(listMap, "4600025748", "绝缘导线", "AC10kV,JKLGYJ,150/30", 2500, "米", "盘号:A2");        addTestData(listMap, "4600025749", "绝缘导线", "AC10kV,JKLGYJ,150/30", 3500, "米", "盘号:A3");        addTestData(listMap, "4600025750", "绝缘导线", "AC10kV,JKLGYJ,150/30", 4500, "米", "盘号:A4");        addTestData(listMap, "4600025751", "绝缘导线", "AC10kV,JKLGYJ,150/30", 3800, "米", "盘号:A5");        addTestData(listMap, "4600025752", "绝缘导线", "AC10kV,JKLYJ,180", 2000, "米", "盘号:A6");        addTestData(listMap, "4600025753", "绝缘导线", "AC10kV,JKLYJ,120", 4200, "米", "盘号:A7");        addTestData(listMap, "4600025754", "绝缘导线", "AC10kV,JKLYJ,120", 3700, "米", "盘号:A8");        addTestData(listMap, "4600025755", "绝缘导线", "AC10kV,JKLYJ,120", 4300, "米", "盘号:A9");        addTestData(listMap, "4600025756", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2800, "米", "盘号:A10");        addTestData(listMap, "4600025757", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2400, "米", "盘号:A11");        addTestData(listMap, "4600025758", "绝缘导线", "AC10kV,JKLGYJ,100/20", 2600, "米", "盘号:A12");         HashMap<String, Object> map = new HashMap<>();        map.put("qdList", listMap);  // 添加供货清单数据        map.put("contacts", "张三");  // 联系人        map.put("contactsPhone", "13988887777");  // 联系电话        map.put("date", "2025年01月18日");  // 日期        map.put("company", "新电缆科技有限公司");  // 公司名称        map.put("customer", "国网北京市电力公司");  // 客户         String wordName = "template.ftl"; // FreeMarker模板文件名        String fileName = "供货清单" + System.currentTimeMillis() + ".doc"; // 带时间戳的文件名        String name = "name";  // 临时文件名         // 确保输出目录存在        File directoryFile = new File(directory);        if (!directoryFile.exists()) {            directoryFile.mkdirs();  // 如果目录不存在则创建        }         // 生成Word文件        WordUtil.exportMillCertificateWord(directory, map, wordName, fileName, name);         System.out.println("文件成功生成在:" + directory + fileName);    }     private void addTestData(List<Map<String, Object>> listMap, String danhao, String name, String model, int num, String unit, String remark) {        Map<String, Object> item = new HashMap<>();        item.put("serNo", listMap.size() + 1);  // 序号        item.put("danhao", danhao);  // 单号        item.put("name", name);  // 产品名称        item.put("model", model);  // 规格型号        item.put("num", String.valueOf(num));  // 数量,转换为字符串        item.put("unit", unit);  // 单位        item.put("remark", remark);  // 备注        listMap.add(item);  // 将数据添加到列表    }}2.WordUtil工具类package com.pw.utils; import freemarker.template.Configuration;import freemarker.template.Template; import java.io.*;import java.util.Map; public class WordUtil {    private static Configuration configuration = null;     // 模板文件夹路径    private static final String templateFolder = WordUtil.class.getResource("/templates").getPath();     static {        configuration = new Configuration();        configuration.setDefaultEncoding("utf-8");        try {            System.out.println(templateFolder);            configuration.setDirectoryForTemplateLoading(new File(templateFolder));  // 设置模板加载路径        } catch (IOException e) {            e.printStackTrace();        }    }     private WordUtil() {        throw new AssertionError();  // 防止实例化    }     /**     * 导出Word文档     * @param map Word文档中参数     * @param wordName 模板的名字,例如xxx.ftl     * @param fileName Word文件的名字 格式为:"xxxx.doc"     * @param outputDirectory 输出文件的目录路径     * @param name 临时的文件夹名称,作为Word文件生成的标识     * @throws IOException     */    public static void exportMillCertificateWord(String outputDirectory, Map map, String wordName, String fileName, String name) throws IOException {        Template freemarkerTemplate = configuration.getTemplate(wordName);  // 获取模板文件        File file = null;        try {            // 调用工具类的createDoc方法生成Word文档            file = createDoc(map, freemarkerTemplate, name);             // 确保输出目录存在            File dir = new File(outputDirectory);            if (!dir.exists()) {                dir.mkdirs();  // 如果目录不存在则创建            }             // 定义完整的文件路径            File outputFile = new File(outputDirectory, fileName);             // 重命名并移动文件到指定目录            file.renameTo(outputFile);             System.out.println("文件成功生成在: " + outputFile.getAbsolutePath());        } finally {            if (file != null && file.exists()) {                file.delete();  // 删除临时文件            }        }    }     private static File createDoc(Map<?, ?> dataMap, Template template, String name) {        File f = new File(name);        try {            // 使用OutputStreamWriter来指定编码,防止特殊字符出问题            Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");            template.process(dataMap, w);  // 使用FreeMarker处理模板            w.close();        } catch (Exception ex) {            ex.printStackTrace();            throw new RuntimeException(ex);        }        return f;  // 返回生成的文件    }}3.FreeMarker模版<!DOCTYPE html><html><head>    <meta charset="UTF-8">    <title>${company}送货清单</title>    <style>        body { font-family: SimSun, serif; }  <!-- 设置字体 -->        table { border-collapse: collapse; width: 100%; }  <!-- 设置表格样式 -->        th, td { border: 1px solid black; padding: 5px; text-align: center; }  <!-- 设置表格的单元格样式 -->        th { background-color: #f2f2f2; }  <!-- 设置表头背景色 -->        .subtotal { font-weight: bold; }  <!-- 小计行加粗 -->        .total { font-weight: bold; font-size: 1.1em; }  <!-- 总计行加粗并设置字体大小 -->    </style></head><body><h1 style="text-align: center;">${company}送货清单</h1>  <!-- 顶部公司名称 --> <table>    <tr>        <th>序号</th>  <!-- 表头:序号 -->        <th>供货单号</th>  <!-- 表头:供货单号 -->        <th>产品名称</th>  <!-- 表头:产品名称 -->        <th>规格型号</th>  <!-- 表头:规格型号 -->        <th>数量</th>  <!-- 表头:数量 -->        <th>单位</th>  <!-- 表头:单位 -->        <th>备注</th>  <!-- 表头:备注 -->    </tr>    <#assign totalQuantity = 0>  <!-- 总数量初始化 -->    <#assign totalItems = 0>  <!-- 总项数初始化 -->    <#assign sortedList = qdList?sort_by("model")>  <!-- 按照规格型号排序 -->    <#assign currentModel = "">  <!-- 当前型号初始化 -->    <#assign subtotalQuantity = 0>  <!-- 小计数量初始化 -->    <#assign subtotalItems = 0>  <!-- 小计项数初始化 -->    <#list sortedList as item>  <!-- 遍历排序后的列表 -->        <#if item.model != currentModel>  <!-- 如果规格型号变了 -->            <#if currentModel != "">  <!-- 如果当前规格型号不是空 -->                <tr class="subtotal">                    <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td>                    <td>${subtotalQuantity}</td>                    <td>${sortedList[0].unit}</td>                    <td></td>                </tr>            </#if>            <#assign currentModel = item.model>  <!-- 更新当前型号 -->            <#assign subtotalQuantity = 0>  <!-- 重置小计数量 -->            <#assign subtotalItems = 0>  <!-- 重置小计项数 -->        </#if>        <tr>            <td>${item?counter}</td>  <!-- 序号 -->            <td>${item.danhao}</td>  <!-- 单号 -->            <td>${item.name}</td>  <!-- 产品名称 -->            <td>${item.model}</td>  <!-- 规格型号 -->            <td>${item.num}</td>  <!-- 数量 -->            <td>${item.unit}</td>  <!-- 单位 -->            <td>${item.remark}</td>  <!-- 备注 -->        </tr>        <#assign itemNum = item.num?replace(",", "")?number>  <!-- 将数量转为数字并处理逗号 -->        <#assign subtotalQuantity = subtotalQuantity + itemNum>  <!-- 累加小计数量 -->        <#assign subtotalItems = subtotalItems + 1>  <!-- 累加小计项数 -->        <#assign totalQuantity = totalQuantity + itemNum>  <!-- 累加总数量 -->        <#assign totalItems = totalItems + 1>  <!-- 累加总项数 -->    </#list>    <#if currentModel != "">  <!-- 如果当前规格型号不是空 -->        <tr class="subtotal">            <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td>            <td>${subtotalQuantity}</td>            <td>${sortedList[0].unit}</td>            <td></td>        </tr>    </#if>    <tr class="total">        <td colspan="4">合计:${totalQuantity}${qdList[0].unit} ${totalItems}轴</td>        <td>${totalQuantity}</td>        <td>${qdList[0].unit}</td>        <td></td>    </tr></table> <p>发货联系人:${contacts}</p>  <!-- 发货联系人 --><p>联系电话:${contactsPhone}</p>  <!-- 联系电话 --><p>日期:${date}</p>  <!-- 日期 --> <p style="text-align: right;">收货人(签字):_______________</p>  <!-- 收货人签字 --><p style="text-align: right;">联系电话:_______________</p>  <!-- 收货人联系电话 --><p style="text-align: right;">${customer}</p>  <!-- 客户 --></body></html>4.POM依赖<!-- freemarker依赖,用于模板引擎,方便进行页面的渲染和数据的展示等操作 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-freemarker</artifactId></dependency><!-- Apache POI 的核心依赖,用于操作 Microsoft Office 格式的文档,如 Excel、Word 等文件 --><dependency>    <groupId>org.apache.poi</groupId>    <artifactId>poi</artifactId>    <version>5.0.0</version></dependency><!-- Apache POI 的 OOXML 扩展依赖,主要用于处理 Office 2007 及以后版本的 OOXML 格式的文件,例如.xlsx 等 --><dependency>    <groupId>org.apache.poi</groupId>    <artifactId>poi-ooxml</artifactId>    <version>5.0.0</version></dependency><!-- OOXML 模式相关的依赖,提供了对 OOXML 文档结构和内容模式的支持,有助于 Apache POI 更好地操作 OOXML 格式文件 --><dependency>    <groupId>org.apache.poi</groupId>    <artifactId>ooxml-schemas</artifactId>    <version>1.4</version></dependency>WordController类深度解析WordController类是整个应用的核心控制器,负责协调数据生成和文档创建的过程。让我们逐步分析它的主要组成部分:1.类结构public class WordController {    // 方法定义...}这个类没有继承任何其他类,也没有实现任何接口,是一个独立的控制器类。2.main方法public static void main(String[] args) throws IOException {    String filePath = "F:\\Poi2Word\\src\\main\\resources\\output";    new WordController().generateWordFile(filePath);}这是应用的入口点。它设置了输出文件的路径,然后调用generateWordFile方法。请注意:在常规的 Spring Boot 实际应用场景下,我们一般不会直接在控制器类中使用 main 方法。此处之所以将 main 方法置于控制器中,纯粹是出于演示目的,旨在让相关流程更加直观易懂。而当进入到正式开发环节时,有几个关键要点务必落实:其一,需要引入数据库集成功能,将当前所使用的测试数据全面替换为从数据库中精准查询获取的真实数据,以此确保数据的准确性与时效性;其二,要对控制器进行优化改造,摒弃现有的演示模式,将其转换为遵循标准规范的请求接口实现方式,进而满足实际业务需求,提升系统的稳定性与可扩展性。3.generateWordFile方法此方法的只要目的是生成Word文件,首先需要先收集和存储测试数据,存储表格数据是将一条数据存储在Map集合中,再将每一条数据存储到List集合中。将其他数据存储到单独的一个Map集合中。然后确保输出目录存在,最后调用WordUtil中的exportMillCertificateWord方法生成文件,并输出文件的生成位置。// 生成 Word 文件的方法public void generateWordFile(String directory) throws IOException {    // 存储测试数据的列表,每个元素都是一个 Map,存储了具体的信息    List<Map<String, Object>> listMap = new ArrayList<>();     // 添加测试数据,调用 addTestData 方法添加一条记录    addTestData(listMap, "4600025747", "绝缘导线", "AC10kV,JKLYJ,300", 1500, "米", "盘号:A1");    //... 可以继续调用 addTestData 方法添加更多测试数据...     // 存储最终要填充到 Word 模板的数据的 Map,包含各种信息    HashMap<String, Object> map = new HashMap<>();    // 将测试数据列表添加到 map 中,键为 "qdList"    map.put("qdList", listMap);    // 联系人信息    map.put("contacts", "张三");    // 联系人电话    map.put("contactsPhone", "13988887777");    // 日期信息    map.put("date", "2025年01月18日");    // 公司名称    map.put("company", "新电缆科技有限公司");    // 客户名称    map.put("customer", "国网北京市电力公司");     // Word 模板文件的名称    String wordName = "template.ftl";    // 生成的 Word 文件的名称,使用当前时间戳保证文件名的唯一性    String fileName = "供货清单" + System.currentTimeMillis() + ".doc";    // 名称信息,具体含义可能根据实际情况而定    String name = "name";     // 创建一个文件对象,用于表示输出目录    File directoryFile = new File(directory);    // 检查输出目录是否存在,如果不存在则创建目录    if (!directoryFile.exists()) {        directoryFile.mkdirs();    }     // 调用 WordUtil 的 exportMillCertificateWord 方法生成 Word 文件    // 传入目录、数据 Map、模板名称、生成的文件名称和名称信息    WordUtil.exportMillCertificateWord(directory, map, wordName, fileName, name);     // 打印生成文件的成功信息    System.out.println("文件成功生成在:" + directory + fileName);}这个方法完成以下任务:创建一个一个List<Map<String,Object>>集合来存储供货清单数据使用addTestData方法添加多条测试数据创建一个Map集合来存储企业名称,发货联系人,联系电话等信息确保输出目录存在调用WordUtil.exportMillCertificateWord方法来生成Word文档4.addTestData方法这个方法用于创建单个供货项目的数据// 添加一条测试数据到 listMap 中private void addTestData(List<Map<String, Object>> listMap, String danhao, String name, String model, int num, String unit, String remark) {    // 创建一个新的 HashMap,用于存储每一条数据    Map<String, Object> item = new HashMap<>();        // 将数据项依次放入 HashMap 中,"serNo" 表示序号,使用 listMap 的大小+1 生成序号    item.put("serNo", listMap.size() + 1);  // 序号是当前列表的大小 + 1    item.put("danhao", danhao);  // 供货单号    item.put("name", name);  // 产品名称    item.put("model", model);  // 规格型号    item.put("num", String.valueOf(num));  // 数量,将整数转为字符串    item.put("unit", unit);  // 单位    item.put("remark", remark);  // 备注        // 将该条数据项添加到 listMap 列表中    listMap.add(item);}这个方法完成以下任务:它接收多个参数,代表一个供货项目的各个属性。创建一个新的Map来存储这个项目的数据。自动计算序号(serNo)基于当前列表的大小。将所有数据添加到Map中。将这个Map添加到供货清单列表中。WordUtil类深度解析WordUtil类是整个文档生成过程的核心,它封装了FreeMarker模板引擎的配置和使用逻辑。让我们逐步分析它的主要组成部分:1.类结构和静态成员public class WordUtil {    private static Configuration configuration = null;    private static final String templateFolder = WordUtil.class.getResource("/templates").getPath();        // 其他方法...}configuration:这是FreeMarker的核心配置对象,用于设置模版加载路径。templateFolder:定义了模版文件的存储路径。使用getResource()方法确保在不同环境下都能正确找到模版文件。2.静态初始化块这段代码的作用是初始化FreeMarker的Configuration对象,设置模版加载目录以及编码格式,以便FreeMarker后续能够正确加载和处理模版文件。// 静态初始化块,用于初始化 FreeMarker 配置static {    // 创建一个 FreeMarker 配置对象,用于后续模板处理    configuration = new Configuration();        // 设置 FreeMarker 配置对象的默认编码为 "utf-8"    configuration.setDefaultEncoding("utf-8");        try {        // 输出模板文件夹路径,帮助调试        System.out.println(templateFolder);                // 设置模板加载目录为 templateFolder 指定的路径,模板文件会从该目录加载        configuration.setDirectoryForTemplateLoading(new File(templateFolder));    } catch (IOException e) {        // 如果加载模板目录时出现异常,打印错误堆栈信息        e.printStackTrace();    }}这个静态初始化块在类加载时执行,主要完成以下任务:创建FreeMarker的Configuration对象设置默认编码为UTF-8,确保正确处理中文等字符设置模版加载目录,这样FreeMarker就知道从哪里查找加载模版文件了错误处理:如果执行过程中出现了IO异常,就会打印堆栈跟踪3.私有构造函数这个构造函数防止类被实例化,确保WordUtil只能通过其静态方法使用。private WordUtil() {    throw new AssertionError();}私有构造函数的好处包括:防止类被实例化当类的构造函数被声明为private时,外部代码无法直接创建该类的实例。这就意味着该类只能公国静态方法访问,确保类的功能是全局共享的。实现单例模式的基础在一些设计模式中,例如单例模式,类只允许有一个实例,私有构造函数确保了这一点。通过private构造函数,我们可以控制类的实例化过程,并确保只有一个实例被创建。封装类的内部实现私有构造函数可以帮助隐藏类的具体实现细节,外部代码不需要关心如何创建类的实例,只需要使用类提供的静态方法即可。这增加了类的封装性,降低了与外部代码的耦合度。避免多余的对象创建由于无法实例化类,每次调用静态方法时,都会使用已有的类实例,这可以避免无意义的对象创建,节省内存和资源。4.exportMillCertificateWord方法这个方法的主要功能是通过加载指定的 FreeMarker 模板生成一个临时的 Word 文档,确保输出目录存在后,将临时文件重命名并保存到指定的位置,同时在过程结束后清理临时文件,并打印文件生成的成功消息。// 导出 Word 文档的方法public static void exportMillCertificateWord(String outputDirectory, Map map, String wordName, String fileName, String name) throws IOException {    // 获取 FreeMarker 模板文件    Template freemarkerTemplate = configuration.getTemplate(wordName);    // 初始化一个 File 对象,用于存储生成的临时文件    File file = null;        try {        // 使用模板和数据创建 Word 文档,返回临时文件        file = createDoc(map, freemarkerTemplate, name);                // 创建目标目录的 File 对象        File dir = new File(outputDirectory);                // 如果目录不存在,则创建该目录        if (!dir.exists()) {            dir.mkdirs();  // 创建目录及其父目录        }                // 定义最终输出文件的完整路径(包括目录和文件名)        File outputFile = new File(outputDirectory, fileName);                // 将临时生成的文件重命名为目标文件,并将其移动到指定目录        file.renameTo(outputFile);                // 打印输出文件的绝对路径,a通知文件生成成功        System.out.println("文件成功生成在: " + outputFile.getAbsolutePath());    } finally {        // 最后,无论是否成功生成文件,都确保临时文件被删除        if (file != null && file.exists()) {            file.delete();  // 删除临时文件        }    }}这个方法是文档导出的主要入口,主要实现了以下功能:加载指定的FreeMarker模版调用createDoc方法生成临时文档文件确保输出目录存在将临时文件重命名并移动到指定的输出位置使用finally块确保临时文件被删除,无论过程是否成功5.createDoc方法这个方法是创建文档的核心方法,主要是通过创建一个临时文件,使用指定的FreeMarker模版和数据模型将内容填充到文件中,并确保文件使用UTF-8编码进行写入。该方法在执行过程中捕获异常并打印堆栈信息,确保发生错误时能够正确处理。最后。方法返回生成的文件对象,以便后续操作或保存。// 创建文档的方法,使用 FreeMarker 模板生成内容并写入文件private static File createDoc(Map<?, ?> dataMap, Template template, String name) {    // 创建一个新的 File 对象,表示生成的文档文件,文件名由参数 "name" 提供    File f = new File(name);        try {        // 使用 OutputStreamWriter 创建一个写入文件的 Writer 对象,设置编码为 "utf-8"        Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");                // 使用 FreeMarker 模板将数据填充到文件中        template.process(dataMap, w);                // 关闭 Writer,确保所有内容写入文件        w.close();    } catch (Exception ex) {        // 捕获异常并打印错误堆栈信息        ex.printStackTrace();                // 抛出 RuntimeException,确保错误被传播到调用者        throw new RuntimeException(ex);    }        // 返回生成的文件对象    return f;}这个方法是实际创建文档的核心,主要实现以下功能:创建一个临时文件。使用OutputStreamWriter设置UTF-8编码,确保正确处理所有字符。调用FreeMarker的template.process()方法,将数据模型(dataMap)应用到模板上。关闭写入器。如果过程中发生异常,打印堆栈跟踪并抛出RuntimeException。返回生成的文件对象。6.WordUtil类总结WordUtil 类通过封装 FreeMarker 模板引擎的配置和文件操作,提供了一个简洁的文档生成工具。它加载指定模板,使用数据模型填充内容,创建临时文件,并确保文件按照指定路径保存。该类通过静态方法确保全局共享功能,使用 UTF-8 编码处理字符,捕获异常并清理临时文件,确保文档生成过程的稳定性和高效性。FreeMarker模板深度解析FreeMarker模板是整个文档生成过程的核心,它定义了最终Word文档的结构和样式。让我们来逐步分析模板的主要组成部分1.文档结构和样式<!DOCTYPE html> <!-- 声明文档类型为 HTML5 --><html><head>    <!-- 设置文档字符编码为 UTF-8,支持中文和其他字符集 -->    <meta charset="UTF-8">    <!-- 设置页面标题,动态插入公司名称 -->    <title>${company}送货清单</title>    <style>        /* 设置页面正文的字体为 SimSun(宋体),如果没有则使用 serif */        body { font-family: SimSun, serif; }         /* 设置表格样式:表格边框合并,宽度100% */        table { border-collapse: collapse; width: 100%; }         /* 设置表格头部和单元格的边框、内边距和文本居中对齐 */        th, td { border: 1px solid black; padding: 5px; text-align: center; }         /* 设置表头背景色为浅灰色 */        th { background-color: #f2f2f2; }         /* 设置小计行字体加粗 */        .subtotal { font-weight: bold; }         /* 设置合计行字体加粗,字体大小稍大 */        .total { font-weight: bold; font-size: 1.1em; }    </style></head><body>    <!-- 页面标题,居中显示公司名称和送货清单 -->    <h1 style="text-align: center;">${company}送货清单</h1>    <!-- 表格内容将在这里生成,动态插入数据 --></body></html>这段代码通过HTML和内嵌CSS定义了页面布局和样式:动态公司名称:<title>标签使用${company}插入动态的公司名称,显示在浏览器标签中。字体和表格样式:设置页面字体为宋体(Simsun)定义表格边框合并、100%宽度,并使单元格内容居中小计和总计行样式:为小计行加粗字体,并为总计行加粗且增大字体,突出显示重要数据。2.表格结构和动态数据插入<table>    <!-- 表头,定义表格的列名 -->    <tr>        <th>序号</th>  <!-- 序号 -->        <th>供货单号</th>  <!-- 供货单号 -->        <th>产品名称</th>  <!-- 产品名称 -->        <th>规格型号</th>  <!-- 规格型号 -->        <th>数量</th>  <!-- 数量 -->        <th>单位</th>  <!-- 单位 -->        <th>备注</th>  <!-- 备注 -->    </tr>     <!-- 初始化总计和小计相关变量 -->    <#assign totalQuantity = 0>  <!-- 总数量 -->    <#assign totalItems = 0>  <!-- 总项数 -->    <#assign sortedList = qdList?sort_by("model")>  <!-- 按照规格型号对数据进行排序 -->    <#assign currentModel = "">  <!-- 当前规格型号 -->    <#assign subtotalQuantity = 0>  <!-- 小计数量 -->    <#assign subtotalItems = 0>  <!-- 小计项数 -->     <!-- 遍历排序后的列表 -->    <#list sortedList as item>        <!-- 如果当前项的规格型号与上一项不同,则输出上一项的小计 -->        <#if item.model != currentModel>            <#if currentModel != "">                <!-- 输出上一规格型号的小计行 -->                <tr class="subtotal">                    <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td>                    <td>${subtotalQuantity}</td>                    <td>${sortedList[0].unit}</td>                    <td></td>                </tr>            </#if>            <!-- 更新当前规格型号为当前项的规格型号,并重置小计 -->            <#assign currentModel = item.model>            <#assign subtotalQuantity = 0>            <#assign subtotalItems = 0>        </#if>         <!-- 输出当前行数据 -->        <tr>            <td>${item?counter}</td>  <!-- 序号,使用 FreeMarker 的 counter 计数 -->            <td>${item.danhao}</td>  <!-- 供货单号 -->            <td>${item.name}</td>  <!-- 产品名称 -->            <td>${item.model}</td>  <!-- 规格型号 -->            <td>${item.num}</td>  <!-- 数量 -->            <td>${item.unit}</td>  <!-- 单位 -->            <td>${item.remark}</td>  <!-- 备注 -->        </tr>         <!-- 更新小计和总计的数量和项数 -->        <#assign itemNum = item.num?replace(",", "")?number>  <!-- 将数量转为数字并处理逗号 -->        <#assign subtotalQuantity = subtotalQuantity + itemNum>  <!-- 累加小计数量 -->        <#assign subtotalItems = subtotalItems + 1>  <!-- 累加小计项数 -->        <#assign totalQuantity = totalQuantity + itemNum>  <!-- 累加总数量 -->        <#assign totalItems = totalItems + 1>  <!-- 累加总项数 -->    </#list>     <!-- 如果最后一项有数据,输出最后的规格型号小计 -->    <#if currentModel != "">        <tr class="subtotal">            <td colspan="4">小计:${subtotalQuantity}${sortedList[0].unit} ${subtotalItems}轴</td>            <td>${subtotalQuantity}</td>            <td>${sortedList[0].unit}</td>            <td></td>        </tr>    </#if>     <!-- 输出最终的合计行 -->    <tr class="total">        <td colspan="4">合计:${totalQuantity}${qdList[0].unit} ${totalItems}轴</td>  <!-- 显示合计的数量和项数 -->        <td>${totalQuantity}</td>  <!-- 合计数量 -->        <td>${qdList[0].unit}</td>  <!-- 单位 -->        <td></td>    </tr></table>表格结构:使用 <table> 标签创建表格,并通过 <th> 定义表头,包含7列:序号、供货单号、产品名称等。动态数据插入:使用 FreeMarker <#list> 遍历排序后的清单数据,并通过 ${item.属性名} 动态插入每项数据,如 ${item.danhao} 插入供货单号。小计和总计计算:通过 <#assign> 定义变量如 totalQuantity 和 subtotalQuantity,在循环中累加数量。使用 <#if> 判断条件,插入小计行,并在循环结束后插入总计行。数据处理:使用 sortedList = qdList?sort_by("model") 按型号对清单数据进行排序。处理数量 itemNum = item.num?replace(",", "")?number,移除逗号并转换为数字,确保计算正确。格式化输出:小计和总计行使用 colspan 属性合并单元格,确保表格显示整洁。使用 CSS 类 subtotal 和 total 为小计和总计行应用加粗和突出显示的样式。总结:此表格通过 FreeMarker 动态插入数据、计算小计和总计,并通过合适的排序和格式化样式,确保清单展示清晰且易于阅读。最后,模板还包括了一些额外信息:<p>发货联系人:${contacts}</p><p>联系电话:${contactsPhone}</p><p>日期:${date}</p> <p style="text-align: right;">收货人(签字):_______________</p><p style="text-align: right;">联系电话:_______________</p><p style="text-align: right;">${customer}</p>这部分添加了额外的联系信息和签名区域,进一步完善了文档的实用性。总的来,这个FreeMarker模板展示了如何结合HTML、CSS和FreeMarker的模板语法来创建一个复杂、动态且格式良好的文档。它不仅能够准确地呈现数据,还能执行必要的计算和格式化,从而生成一个专业的供货清单文档。总结通过使用SpingBoot、Apache POI和FreeMarker,我们成功自动化了电缆供货清单的生成过程。这不仅提高了效率,还减少了人为错误。本解决方案的模块化设计使其易于维护和扩展。希望本教程能够帮助您理解如何使用Java技术来解决实际业务问题。————————————————原文链接:https://blog.csdn.net/weixin_66401877/article/details/145230273
  • [技术干货] 【java-数据结构】Java 二叉树:代码世界里的神奇树形魔法
    前言在计算机科学领域,数据结构是算法实现的基石,而二叉树则是其中一颗璀璨的明珠。它以独特的树形结构,在众多场景中发挥着关键作用。二叉树由节点组成,每个节点最多包含两个子节点,这种简洁而强大的设计,使得数据的存储、检索与处理变得高效且有序。无论是数据库索引、编译器的语法分析,还是人工智能中的决策树算法,都离不开二叉树的身影。在Java编程世界里,掌握二叉树的生成与操作是迈向高级编程的重要一步。通过使用Java语言来构建二叉树,不仅能够深入理解数据结构的底层原理,还能提升解决复杂问题的能力。接下来,我们将一步步深入探索如何在Java中实现二叉树,从节点的定义到树的构建,再到各种遍历与操作方法,揭开这一重要数据结构的神秘面纱。一:什么是二叉树二叉树是一种每个节点最多有两个子节点的树形数据结构,这两个子节点通常被称为左子节点和右子节点。这种简洁而强大的结构,在许多算法和数据处理场景中发挥着关键作用。树是⼀种⾮线性的数据结构,它是由n(n>=0)个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。它具有以下的特点:有⼀个特殊的结点,称为根结点,根结点没有前驱结点除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、…、Tm,其中每⼀个集合Ti(1 <= i <= m) ⼜是⼀棵与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有0个或多个后继树是递归定义的。注意:树形结构中,⼦树之间不能有交集,否则就不是树形结构那么怎么区分什么是树,什么不是树呢?请看下面图片: 一些二叉树重要的概念,以下面这棵树为例:结点的度:⼀个结点含有⼦树的个数称为该结点的度; 如上图:A的度为6树的度:⼀棵树中,所有结点度的最⼤值称为树的度; 如上图:树的度为6叶⼦结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I…等节点为叶结点双亲结点或⽗结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点; 如上图:A是B的⽗结点孩⼦结点或⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点; 如上图:B是A的孩⼦结点根结点:⼀棵树中,没有双亲结点的结点;如上图:A结点的层次:从根开始定义起,根为第1层,根的⼦结点为第2层,以此类推树的⾼度或深度:树中结点的最⼤层次; 如上图:树的⾼度为4树的以下概念只需了解,在看书时只要知道是什么意思即可:⾮终端结点或分⽀结点:度不为0的结点; 如上图:D、E、F、G…等节点为分⽀结点兄弟结点:具有相同⽗结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点堂兄弟结点:双亲在同⼀层的结点互为堂兄弟;如上图:H、I互为兄弟结点结点的祖先:从根到该结点所经分⽀上的所有结点;如上图:A是所有结点的祖先⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙森林:由m(m>=0)棵互不相交的树组成的集合称为森林二:Java中二叉树的实现2.1定义二叉树节点类首先,我们需要定义一个类来表示二叉树的节点。每个节点包含一个数据元素,以及指向左子节点和右子节点的引用。class TreeNode {    int data;    TreeNode left;    TreeNode right;    TreeNode(int data) {        this.data = data;        this.left = null;        this.right = null;    }}2.2构建二叉树接下来,我们可以编写代码来构建一棵简单的二叉树。public class BinaryTreeMagic {    public static void main(String[] args) {        // 创建根节点        TreeNode root = new TreeNode(1);        // 创建左子节点        root.left = new TreeNode(2);        // 创建右子节点        root.right = new TreeNode(3);        // 为左子节点创建左子节点        root.left.left = new TreeNode(4);        // 为左子节点创建右子节点        root.left.right = new TreeNode(5);        // 至此,一棵简单的二叉树构建完成        //       1        //      / \        //     2   3        //    / \        //   4   5    }}2.3二叉树的遍历遍历二叉树是对树中每个节点进行访问的过程。常见的遍历方式有三种:前序遍历、中序遍历和后序遍历。学习⼆叉树结构,最简单的⽅式就是遍历。所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做⼀次且仅做⼀次访问。访问结点所做的操作依赖于具体的应⽤问题(⽐如:打印节点内容、节点内容加1)。 遍历是⼆叉树上最重要的操作之⼀,是⼆叉树上进⾏其它运算之基础。在遍历⼆叉树时,如果没有进⾏某种约定,每个⼈都按照⾃⼰的⽅式遍历,得出的结果就⽐较混乱,如果按照某种规则进⾏约定,则每个⼈对于同⼀棵树的遍历结果肯定是相同的。如果N代表根节点,L代表根节点的左⼦树,R代表根节点的右⼦树,则根据遍历根节点的先后次序有以下遍历⽅式:• NLR:前序遍历(Preorder Traversal 亦称先序遍历)⸺访问根结点—>根的左⼦树—>根的右⼦树。• LNR:中序遍历(Inorder Traversal)⸺根的左⼦树—>根节点—>根的右⼦树。• LRN:后序遍历(Postorder Traversal)⸺根的左⼦树—>根的右⼦树—>根节点2.3.1前序遍历前序遍历的顺序是先访问根节点,然后递归地访问左子树,最后递归地访问右子树。void preOrderTraversal(TreeNode node) {    if (node!= null) {        System.out.print(node.data + " ");        preOrderTraversal(node.left);        preOrderTraversal(node.right);    }}2.3.2中序遍历中序遍历的顺序是先递归地访问左子树,然后访问根节点,最后递归地访问右子树。void inOrderTraversal(TreeNode node) {    if (node!= null) {        inOrderTraversal(node.left);        System.out.print(node.data + " ");        inOrderTraversal(node.right);    }}2.3.3后序遍历后序遍历的顺序是先递归地访问左子树,然后递归地访问右子树,最后访问根节点。void postOrderTraversal(TreeNode node) {    if (node!= null) {        postOrderTraversal(node.left);        postOrderTraversal(node.right);        System.out.print(node.data + " ");    }}前序遍历结果:1 2 3 4 5 6中序遍历结果:3 2 1 5 4 6后序遍历结果:3 2 5 6 4 12.3.4 层序遍历层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对⼆叉树进⾏层序遍历。设⼆叉树的根节点所在层数为1,层序遍历就是从所在⼆叉树的根节点出发,⾸先访问第⼀层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,⾃上⽽下,⾃左⾄右逐层访问树的结点的过程就是层序遍历。以这棵树为例写下面题目:1.某完全⼆叉树按层次输出(同⼀层从左到右)的序列为 ABCDEFGH 。该完全⼆叉树的前序序列为(A)A: ABDHECFGB: ABCDEFGHC: HDBEAFCGD: HDEBFGCA2.⼆叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则⼆叉树根结点为(A)A: EB: FC: GD: H3.设⼀课⼆叉树的中序遍历序列:badce,后序遍历序列:bdeca,则⼆叉树前序遍历序列为(D)A: adbceB: decabC: debacD: abcde4.某⼆叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同⼀层从左到右)的序列为(A)A: FEDCBAB: CBAFEDC: DEFCBAD: ABCDEF2.4完整代码示例下面是一个包含二叉树构建和遍历功能的完整Java代码示例:class TreeNode {    int data;    TreeNode left;    TreeNode right;    TreeNode(int data) {        this.data = data;        this.left = null;        this.right = null;    }}public class BinaryTreeMagic {    void preOrderTraversal(TreeNode node) {        if (node!= null) {            System.out.print(node.data + " ");            preOrderTraversal(node.left);            preOrderTraversal(node.right);        }    }    void inOrderTraversal(TreeNode node) {        if (node!= null) {            inOrderTraversal(node.left);            System.out.print(node.data + " ");            inOrderTraversal(node.right);        }    }    void postOrderTraversal(TreeNode node) {        if (node!= null) {            postOrderTraversal(node.left);            postOrderTraversal(node.right);            System.out.print(node.data + " ");        }    }    public static void main(String[] args) {        BinaryTreeMagic tree = new BinaryTreeMagic();        // 创建根节点        TreeNode root = new TreeNode(1);        // 创建左子节点        root.left = new TreeNode(2);        // 创建右子节点        root.right = new TreeNode(3);        // 为左子节点创建左子节点        root.left.left = new TreeNode(4);        // 为左子节点创建右子节点        root.left.right = new TreeNode(5);        System.out.println("前序遍历:");        tree.preOrderTraversal(root);        System.out.println();        System.out.println("中序遍历:");        tree.inOrderTraversal(root);        System.out.println();        System.out.println("后序遍历:");        tree.postOrderTraversal(root);        System.out.println();    }}三:二叉树的应用场景搜索算法:二叉搜索树(BST)是一种特殊的二叉树,它满足左子树所有节点的值小于根节点的值,右子树所有节点的值大于根节点的值。这种特性使得在BST中进行搜索操作的时间复杂度为O(log n),大大提高了搜索效率。表达式求值:通过构建表达式二叉树,可以方便地对数学表达式进行求值。例如,对于表达式“(3 + 4) * 2”,可以构建相应的二叉树来进行计算。文件系统目录结构:可以用二叉树来模拟文件系统的目录结构,根节点表示根目录,子节点表示子目录或文件,方便进行文件管理和查找。⽂件系统管理(⽬录和⽂件)四:二叉树的重点4.1概念⼀棵⼆叉树是结点的⼀个有限集合,该集合:或者为空或者是由⼀个根节点加上两棵别称为左⼦树和右⼦树的⼆叉树组成从上图可以看出:1. ⼆叉树不存在度⼤于2的结点2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树 注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的:4.2大自然的一些奇观 4.3两种特殊的⼆叉树满⼆叉树: ⼀棵⼆叉树,如果每层的结点数都达到最⼤值,则这棵⼆叉树就是满⼆叉树。也就是说,如果⼀棵⼆叉树的层数为K,且结点总数是 ,则它就是满⼆叉树。完全⼆叉树: 完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为K的,有n个结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从0⾄n-1的结点⼀ 对应时称之为完全⼆叉树。 要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。4.4二叉树的一些性质1. 若规定根结点的层数为1,则⼀棵⾮空⼆叉树的第i层上最多有(i>0)个结点2. 若规定只有根结点的⼆叉树的深度为1,则深度为K的⼆叉树的最⼤结点数是(k>=0)3. 对任何⼀棵⼆叉树, 如果其叶结点个数为 n0, 度为2的⾮叶结点个数为 n2,则有n0=n2+14. 具有n个结点的完全⼆叉树的深度k为上取整5. 对于具有n个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的顺序对所有节点从0开始编号,则对于序号为i的结点有:◦ 若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,⽆双亲结点◦ 若2i+1<n,左孩⼦序号:2i+1,否则⽆左孩⼦◦ 若2i+2<n,右孩⼦序号:2i+2,否则⽆右孩⼦下面一些题目为例:某⼆叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该⼆叉树中的叶⼦结点数为( )A 不存在这样的⼆叉树B 200C 198D 199答案:B2.在具有 2n 个结点的完全⼆叉树中,叶⼦结点个数为( )A nB n+1C n-1D n/2答案:A3.⼀个具有767个节点的完全⼆叉树,其叶⼦节点个数为()A 383B 384C 385D 386答案:B4.⼀棵完全⼆叉树的节点数为531个,那么这棵树的⾼度为( )A 11B 10C 8D 12答案:B五:总结Java二叉树就像代码世界里的神奇魔法棒,通过巧妙地构建和操作树形结构,为我们解决各种复杂的编程问题提供了有力的工具。无论是高效的搜索算法,还是复杂的表达式求值,二叉树都展现出了其独特的魅力和强大的功能。希望通过本文的介绍和代码示例,你能对Java二叉树有更深入的理解和认识,在编程的道路上运用这神奇的树形魔法创造出更多精彩的代码。可以已经准备好进一步探索二叉树的更多高级特性和应用,比如平衡二叉树、红黑树等,它们将为你的编程技能库增添更多强大的武器。————————————————原文链接:https://blog.csdn.net/2301_80350265/article/details/145692883
  • [技术干货] Java的栈与队列以及代码实现
    一.栈(Stack)1.1栈的概念栈:一种特殊的线性表,其 只允许在固定的一端进行插入和删除元素操作 。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO ( Last In First Out )的原则 压栈:栈的插入操作叫做进栈 / 压栈 / 入栈, 入数据在栈顶 。出栈:栈的删除操作叫做出栈。 出数据在栈顶 1.2栈的实现及模拟public class Test {    public static void main(String[] args) {        Stack<Integer>s=new Stack();//创建一个空栈        s.push(1);//往栈中存入1        s.push(2);//2        s.push(3);//3        s.push(4);//4        s.push(5);//5         System.out.println(s.size());//有效个数5        System.out.println(s.peek());//获取栈顶元素5         s.pop();//5出栈         System.out.println(s.peek());//此时栈顶元素变为4         System.out.println(s.empty());//判断是否为空栈,此时不为空 返回false    }} 这里我们用自己的方法来模拟实现上述的方法public class MyStack {    int[] elem;    int usedSize;     public MyStack(){        this.elem=new int[10];    }     public void push(int val){        if(isFull()){            //扩容            elem= Arrays.copyOf(elem,elem.length*2);        }        elem[usedSize]=val;        usedSize++;    }     public boolean isFull(){        return usedSize==elem.length;    }     public int pop(){        if(empty()){            return -1;        }        int oldVal=elem[usedSize-1];        usedSize--;        return oldVal;    }     public int peek(){        if(empty()){            return -1;        }        return elem[usedSize-1];    }     public boolean empty(){        return usedSize==0;    }}二.队列(Queue)2.1队列的概念队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头(Head/Front) 2.2队列的实现及模拟 在Java中,Queue是个接口,底层是通过链表实现的注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。   public class Test{    public static void main(String[] args) {        Queue<Integer>q=new LinkedList<>();        q.offer(1);//从队尾入        q.offer(2);        q.offer(3);        q.offer(4);         System.out.println(q.size());//有效个数 4        System.out.println(q.peek());//获取头元素 1         q.poll();//1从队列中出         System.out.println(q.peek());//2         System.out.println(q.isEmpty());//此时队列不为空,所以返回 false    }}这里我们进行模拟实现上述方法 public class MyQueue {    static class ListNode{        public int val;        public ListNode prev;        public ListNode next;         public ListNode(int val){            this.val=val;        }    }     public ListNode head;    public ListNode last;     public void offer(int val){        ListNode node=new ListNode(val);        if(head==null){            head=last=node;        }else{            last.next=node;            node.prev=last;            last=last.next;        }    }     public int poll(){        if(head==null){            return -1;        }        int ret=head.val;        if(head.next==null){            head=last=null;        }else{            head=head.next;            head.prev=null;        }        return ret;    }     public int peek(){        if(head == null) {            return -1;        }        return head.val;    }     public boolean isEmpty(){        return head==null;    }} 2.3循环队列实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列通常使用数组实现。 2.4双端队列(Deque)双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队 Deque是一个接口,使用时必须创建LinkedList的对象 Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现————————————————                     原文链接:https://blog.csdn.net/2301_80288511/article/details/137435796
  • [技术干货] 带你深入了解前端【HTML+JavaScript】
    关于前端和大家推荐一个书籍,就是JavaScript高级程序设计,也叫红宝书,内容非常全面详细,大家可以买来看,以后面试工作的时候可能会用到,知识点什么的讲解的都挺好的也比较全面1.首先讲解一下src和href的区别:1.src是source的缩写,表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置,src会将其指向的资源下载并应用到文档当中,当浏览器解析到带有src属性的标签时,它会发起一个HTTP请求来加载指定的资源,并将其嵌入到文档中。例如,<img src="image.png">会使浏览器加载并显示图片,当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载,编译,执行完毕,所以一半js脚本会放在页面底部。2.href是hypertext reference的缩写,表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系,当浏览器是识别到它指向的文件时,就会并行下载资源,不会停止对当前文档的处理,常用在a,link,等标签上。当用户点击带有 href属性的链接时,浏览器会导航到指定的URL。例如,<a href="https://example.com">Visit Example</a>会创建一个链接,点击后将用户带到 "example.com"。主要区别:src用于嵌入或替换当前元素的内容,并且src会阻塞页面的解析和渲染,直到资源加载,处理完毕href用于建立链接关系,会并行下载资源,不会阻塞页面的解析和渲染2.HTML语义化语义化是指内容的结构化(内容语义化),选择合适的标签(代码语义化)内容语义化:内容语义化是指在编写HTML时,根据页面的内容和结构,选择最能表达该内容本质的标签来标记内容例如对于文章的标签,应该使用<h1>到<h6>标签,而不是使用<span> 或<div> 并通过样式模拟标签样式。代码语义化:代码语义化是指使用具有明确语义的HTML标签,而不是仅仅依赖class和id或style属性来定义元素的外观和行为,使用<nav>标签来表示页面的导航部分,而不是使用<div>并添加class="nav" 3.script标签中defer和async的区别:如果没有defer和async属性,浏览器会立即加载并执行相应的脚本,它不会等待后续加载的文档元素,读取到就会加载和执行,这样就阻塞了后续的文档的加载。无defer和async属性的script标签当浏览器遇到该标签是,会暂停对HTML的解析和渲染,立即开始加载script.js并执行这会阻塞后续文档元素的加载和解析,直到脚本加载和执行完毕defer属性:当使用<script src="script.js" defer></script〉时,脚本的加载是异步的,不会阻塞 HTML 的解析脚本会在 HTML 文档的解析完成后,按照<script>标签在文档中的出现顺序依次执行在 DoMcontentLoaded 事件触发之前。对于多个带有 defer 属性的脚本,它们会按照在文档中的顺序依次执行,即使它们的加载完成顺序不同。<script src="script1.js" defer></script><script src="script2.is" defer></script><script src="script3.is" defer></script>script2.js和script3.js 会并行加载,script1.is 当HTML解析完成后,按照 script1.js、script2.js、script3.js的顺序依次执行DoMcontentLoaded 事件会在所有 defer 脚本执行完成后触发async属性:当使用<script src="script.js"async>/script〉时,脚本的加载也是异步的,不会阻塞 HTML 的解析。一旦脚本加载完成,会立即执行,不管 HTML 解析是否完成。对于多个带有 async 属性的脚本,它们的执行顺序不保证与在文档中的顺序一致,谁先加载完成谁先护行。<script src="script1.is" defer></script><script src="script2.is" defer></script><script src="script3.js" defer></script>script2.is 和 script3.is 会并行加载,script1.is、它们的执行顺序取决于各自的加载完成时间,可能是 script2.js先执行,然后是 script1.js ,最后是 script3.js,顺序不固定。DoMcontentLoaded 事件可能在某些 async 脚本执行之前或之后触发,因为 async 脚本的执行不依赖于HTML 解析完成,也不遵循<script> 标签的顺序。4.常用的meta标签有哪些,作用分别是什么meta 标签由 name 和 content 属性定义,用来描述网页文档的属性,比如网页的作者,网页描述关键词等,除了HTTP标准固定了一些name作为大家使用的共识,开发者还可以自定义name。charset用于指定 HTML 文档的字符编码,确保浏览器能够正确解析文档中的字符。<meta charset="UTF-8">UTF-8 是一种通用的字符编码,支持世界上大多数语言的字符,确保页面可以正确显示中文、英文、特殊字符等,避免出现乱码现象。keywords为搜索引擎提供页面的关键词信息,帮助搜索引擎更好地理解页面的主题和内容,以提高搜索排名<meta name="keywords" content="关键词1, 关键词2, 关键词3">content 属性包含了一系列用逗号分隔的关键词。例如,对于一个关于旅游的页面,可以使用<meta name="keywords"content="旅游,度假,景点,酒店">。搜索引擎会将这些关键词作为页面的主要搜索词,当用户搜索这些关键词时,该页面可能会出现在搜索结果中description提供页面的简短描述,该描述通常会显示在搜索引擎的搜索结果中,作为页面的摘要信息,<meta name="description" content='这是一个关于旅游景点推荐的页面,为你提供各种热门景点的信息和旅游攻略。">content 属性包含了页面的描述信息,长度通常在 150-160 个字符左右。它可以帮助用户快速了解页面的主要内容,吸引用户点击链接。refresh用于页面的自动刷新或重定向。<meta http-equiv="refresh" content="5;url=https://example.com">content 属性中的第一个值表示刷新或重定向的时间间隔(以秒为单位),上述示例中是5 秒。第二个部分( url=https://example.com )表示重定向的目标 URL。例如,<meta http-equiv="refresh" content="g;url=https://newpage.com">会立即将用户重定向至 https://newpage.com。viewport主要用于控制移动端设备的视口,确保页面在移动设备上的显示效果,使其更适应不同屏幕尺寸和分辨率,<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1.0,user-scalable=no">awidth=device-width :将视口的宽度设置为设备的屏幕宽度,确保页面宽度与设备屏幕宽度匹配initial-scale=1初始缩放比例为 1,即页面初始显示不进行缩放。maximum-scale=1.0限制用户可以将页面放大的最大比例为 1.0 倍,防止用户过度放大页面。user-scalable=no禁止用户手动缩放页面,对于某些特定的页面(如应用页面)可能需要此设置,但通常不建议,因为会影响用户体验。————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/2301_81253185/article/details/145422274
  • [技术干货] 数据结构(Java版)第十期:栈和队列
    一、栈1.1. 栈的概念       栈是⼀种特殊的线性表,其只允许在固定的⼀端进⾏插⼊和删除元素操作。进⾏数据插⼊和删除操作 的⼀端称为栈顶,另⼀端称为栈底。栈中的数据元素遵守后进先出的原则。        入栈:栈的插⼊操作叫做进栈/压栈/⼊栈,⼊数据在栈顶。       出栈:栈的删除操作叫做出栈。出数据在栈顶。 1.2. 栈的使用import java.util.Stack; public class Main {    public static void main(String[] args) {        Stack<Integer> stack = new Stack<>();     }}     我们点进去看一下Stack的源码:public class Stack<E> extends Vector<E> {     public Stack() {    }     public E push(E item) {        addElement(item);         return item;    }      下面是一些Stack的方法,我们来实现一下。 方法    功能Stack()    创建一个空的栈E push(E e)    将e入栈并返回eE pop()    将栈顶元素出栈并返回E peek()    获取栈顶元素int size()    获取栈中有效元素的个数boolean empty()    检查栈是否为空import java.util.Stack; public class Main {    public static void main(String[] args) {        Stack<Integer> stack = new Stack<>();        System.out.println(stack.empty());//检查栈是否为空        stack.push(5);//入栈        stack.push(4);        stack.push(3);        stack.push(2);        stack.push(1);        System.out.println(stack.size());//获取栈的元素个数        System.out.println(stack);        System.out.println(stack.empty());        System.out.println(stack.pop());//栈顶元素出栈        System.out.println(stack.peek());//获取栈顶元素    }}1.3. 栈的模拟实现        Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是 Vector是线程安全的,我们可以直接用数组来实现栈。import java.util.Arrays; public class MyStack {    private int[] elem;    private int usedSize;    private static final int DEFAULT = 10;     public MyStack(){        elem = new int[DEFAULT];    }     public void push(int val){//模拟入栈操作        if(isFull()){//如果满了就扩容            grow();        }        elem[usedSize] = val;        usedSize++;    }     private void grow(){        elem = Arrays.copyOf(elem,elem.length);    }     public boolean isFull(){//判断数组是否满了        return usedSize == elem.length;    }     public int pop() {        if(isEmpty()){            throw new StackIsEmpty("栈为空");        }        usedSize--;//相当于把数据置为null        return elem[usedSize-1];    }     public boolean isEmpty(){        return usedSize == 0;    }     public int peek() {        if(isEmpty()){            throw new StackIsEmpty("栈为空");        }        return elem[usedSize--];    }     public int size(){        return usedSize;    }}public class StackIsEmpty extends RuntimeException{    public StackIsEmpty() {        super();    }     public StackIsEmpty(String message) {        super(message);    }}public class Main {    public static void main(String[] args) {        MyStack stack = new MyStack();        stack.push(1);        stack.push(2);        stack.push(3);        stack.push(4);        stack.push(5);        System.out.println(stack.size());        int num1 = stack.pop();        System.out.println(num1);        int num2 = stack.peek();        System.out.println(num2);    }}二、栈的经典面试题2.1. 逆波兰表达式         逆波兰表达式,又称为后缀表达式,特点是运算符位于操作数之后。例如((2+1)*3)、(4+(13/5))是中缀表达式,转化为逆波兰表达式则为2 1 + 3 *、4 13 5 / +。         本题方法参数给出的是字符串数组,我们要先将字符转化为数字。先用引用去遍历数组,如果是数字,就放入栈中;如果引用指向的是运算符,则将栈顶的两个元素出栈进行运算。如此循环往复,直至遍历完整个字符串数组。import java.util.Stack; public class Solution {    public int evalRPN(String[] tokens){        Stack<Integer> stack = new Stack<>();        int len = tokens.length;        for (int i = 0; i < len; i++) {//遍历字符串数组            String str = tokens[i];            if(!isOperator(str)){                Integer num = Integer.valueOf(str);//将字符转化为数字                stack.push(num);            }else{//如果是运算符,则栈顶两个元素出栈                Integer num1 = stack.pop();                Integer num2 = stack.pop();                switch (str){                    case "+":                        stack.push(num2+num1);                        break;                    case"-":                        stack.push(num2-num1);                        break;                    case"*":                        stack.push(num2*num1);                        break;                    case"/":                        stack.push(num2/num1);                        break;                }            }        }        return stack.pop();    }     private boolean isOperator(String val){//先判断字符串是数字还是运算符        if(val.equals("+") || val.equals("-") || val.equals("*") || val.equals("/")){            return true;        }        return false;    }}2.2. 有效的括号        题目中要求字符串只有大、中、小括号,返回true的条件为:当我们遍历完字符串之后并且栈为空。如果说,当我们左右括号不匹配时,比如"([))",就返回false;当我们遍历完字符串之后,如果栈不为空,也返回false,比如"(()";当我们还没有遍历完字符串,栈已经为空,也会返回false,比如"({}))"。import java.util.Stack; public class Solution {    public boolean isValid(String s){        Stack<Character> stack = new Stack<>();        int len = s.length();        for (int i = 0; i < len; i++) {            char ch1 = s.charAt(i);            if(ch1 == '{' || ch1 == '[' || ch1 == '('){                stack.push(ch1);//将左括号入栈            }else{                if(stack.isEmpty()){                    return false;                }                char ch2 = stack.peek();//获取入栈的左括号,与右括号进行匹配                if((ch2=='{'&&ch1=='}') || (ch2=='['&&ch1==']') || (ch2=='('&&ch1==')')){                    stack.pop();                }else {                    return false;                }            }        }        if(!stack.isEmpty()){            return false;        }        return true;    }}2.3. 最小栈         如果我们抛开这道题,获取栈中的最小元素,我们就可以去遍历这个栈来找出我们的最小元素。但这道题限制我们需要在让时间复杂度为常数,所以说,一个栈是不能解决问题的,还需要在引入一个栈stack。         对于push方法,普通栈当中,所有数据都要放入,最小栈要对我们的普通栈第一次push进行维护。如果最小栈不为空,那么需要比较刚存放的数据与最小栈栈顶的数据进行比较,以对里面的最小值进行更新。这样顺便解决了getMin的实现,直接从最小栈栈顶获取。        对于pop方法,如果弹出的数据不是最小栈的栈顶数据,则只需要弹出普通栈的栈顶数据就行,否则则要弹出最小栈的栈顶数据。top相当于peek方法,只获取普通栈的栈顶元素。       这4个方法基本实现完成,但还有一个问题。如上图所示,如果我们再放入一个-1进入普通栈,那么最小栈需不需要再放入-1呢?答案是需要。因为按照我们的pop方法,把-1弹出,minstack也要弹出。完整代码:import java.util.Stack; public class MinStack {    Stack<Integer> stack = new Stack<>();    Stack<Integer> minStack = new Stack<>();     public MinStack() {        stack = new Stack<>();        minStack = new Stack<>();    }     public void push(int val) {        stack.push(val);        if(minStack.empty()){            minStack.push(val);        }else{            int peekMinVal = minStack.peek();            if(val <= peekMinVal){                minStack.push(val);            }        }    }     public void pop() {        int val = stack.pop();        if(val == minStack.peek()){            minStack.pop();        }    }     public int top() {        return stack.peek();    }     public int getMin() {        return minStack.peek();    }————————————————                    原文链接:https://blog.csdn.net/2401_85198927/article/details/145169124
  • [技术干货] 探索JavaScript前端开发:开启交互之门的神奇钥匙
    引言在当今数字化时代,互联网已然成为人们生活不可或缺的一部分,而网页作为互联网的主要载体,其用户体验的优劣直接关乎着信息的有效传递与用户的留存。JavaScript,这门在前端开发领域占据核心地位的编程语言,犹如一位神奇的魔法师,为静态的网页注入灵动的生命力,使之蜕变成为交互性强、功能丰富的精彩世界。四、事件处理4.1 事件类型在 JavaScript 的前端开发领域,事件处理犹如一座桥梁,紧密连接着用户与网页之间的交互。它能够精准捕捉用户在页面上的各类操作,诸如鼠标的轻轻点击、键盘的敲击输入、表单的提交确认等,并迅速触发相应的 JavaScript 代码来执行特定功能,为用户带来流畅且自然的交互体验。鼠标事件堪称交互中的 “主力军”,涵盖了诸多常见类型。click 事件,作为最为常用的一种,当用户在某个元素上执行单击鼠标左键的操作时,便会如同触动了机关一般被触发,就像在网页上点击一个按钮提交表单,或是打开一个链接跳转页面。与之紧密相关的 dblclick 事件,则要求用户在极短时间内连续双击鼠标左键,常用于实现一些快速操作指令,比如在图片编辑场景下,双击图片快速进入编辑模式。mouseover 与 mouseenter 事件均在鼠标指针移至元素上方时触发,细微差别在于,mouseover 在鼠标经过元素及其子元素时都会触发,而 mouseenter 仅当鼠标初次进入元素自身范围时才触发,二者适用于不同的交互细节需求,如导航菜单的展开,mouseover 能让子菜单在鼠标滑过主菜单及子项时都灵活响应,mouseenter 则可确保仅在精准指向主菜单时才触发展开动作,避免误触。mouseout 与 mouseleave 事件则相反,对应鼠标离开元素的操作,其触发规则与上述类似,常用于收起菜单、隐藏提示信息等场景。mousedown 与 mouseup 事件分别对应鼠标按钮按下与松开的瞬间,这两个事件常与 mousemove 配合,用于实现诸如拖拽元素、绘制图形等复杂交互,像在一些图形设计软件的网页版中,用户按下鼠标并移动来绘制线条,松开鼠标完成绘制。键盘事件同样不可或缺,keydown 事件在用户按下键盘上任意键的瞬间被触发,无论是字母、数字、符号还是功能键,它都能敏锐捕捉,通过监听此事件,开发者可实时获取用户的按键输入,常用于文本输入实时校验、快捷键响应等场景。例如在一些在线文档编辑页面,输入文字时实时检查拼写错误,或是按下 Ctrl + S 组合键触发保存操作。keyup 事件紧随 keydown 之后,在按键松开时触发,与 keydown 配合可精准判断用户完整的按键动作,确保交互逻辑的准确性。需注意的是,曾经的 keypress 事件在按下字符键时触发,但已逐渐被 keydown 取代,因其在处理功能键等方面存在局限。表单事件聚焦于表单元素的交互处理。submit 事件在用户点击表单的提交按钮,或是在表单内按下回车键(前提是表单设置允许)时触发,此时通常会进行表单数据的校验与提交操作,如登录表单验证用户名和密码是否符合格式要求、是否非空等,若校验通过则向服务器发送数据。change 事件则针对表单元素状态的改变,当单选框、复选框被选中或取消选中,下拉列表选择了不同选项,文本框或 textarea 元素内容改变且失去焦点时,都会触发该事件,常用于实时更新相关联的显示内容或执行额外校验,比如电商购物选择商品规格后,实时更新商品总价;在文本框输入完信息,失去焦点时检查格式是否正确。input 事件与 change 类似,不过它更加 “实时”,只要表单元素的值发生变化,便会立即触发,对于实时反馈用户输入极为有用,如搜索框实时显示输入的关键词联想结果。不妨以一个简单实例来深入理解事件驱动机制。在网页上有一个按钮,当用户点击它时,弹出一个提示框显示 “按钮被点击了”:<button id="myButton">点击我</button><script>    document.getElementById('myButton').addEventListener('click', function() {        alert('按钮被点击了');    });</script>在此例中,按钮是事件源,click 是事件类型,而函数 function() { alert('按钮被点击了'); } 便是事件处理程序。当用户执行点击操作这一 “导火索” 时,便迅速激活事件处理程序,进而弹出提示框,完成一次流畅的交互。4.2 事件监听器在 JavaScript 前端开发中,事件监听器是实现高效、灵活事件处理的关键所在。它宛如一位忠诚且机智的 “守护者”,时刻监听着特定 DOM 元素上的各种事件,一旦捕捉到目标事件的发生,便会立即触发与之绑定的相应函数,执行预设的交互逻辑。addEventListener 方法无疑是其中的 “核心利器”,它以一种极为灵活且强大的方式,将事件与处理函数紧密关联起来。其语法结构如下:target.addEventListener(type, listener, useCapture);其中,target 代表目标 DOM 元素,通过诸如 document.getElementById、querySelector 等方法精准获取;type 即为要监听的事件类型,如前文提及的 click、keydown、submit 等;listener 则是对应的事件处理函数,它可以是具名函数,也可以是匿名函数,这个函数承载着当事件触发时需要执行的具体代码逻辑;useCapture 是一个可选参数,用于指定事件冒泡或捕获阶段,默认为 false,即处于冒泡阶段,后续会详细讲解冒泡与捕获机制。以一个为按钮添加点击监听器的实例来深入剖析:<button id="actionButton">执行操作</button><script>    const button = document.getElementById('actionButton');    button.addEventListener('click', function() {        console.log('按钮被点击,即将执行重要操作...');        // 此处可添加具体业务逻辑代码,如发送AJAX请求、更新页面数据等    });</script>在上述代码中,首先通过 document.getElementById 精准定位到 id 为 'actionButton' 的按钮元素,将其赋值给 button 变量。接着,使用 addEventListener 方法为该按钮监听 'click' 事件,当用户点击按钮时,匿名函数内的代码便会立即执行,控制台输出相应提示信息,并且可依需求在此处拓展更为复杂的业务逻辑,如向服务器提交表单数据、动态更新页面 UI 等。与传统的内联事件处理属性相比,addEventListener 具有显著优势。传统的内联方式,如在 HTML 标签内直接使用 onclick="function ()",虽然简单直接,但却将 JavaScript 代码与 HTML 结构紧密耦合在一起,使得代码的维护性与可扩展性大打折扣。一旦业务逻辑复杂起来,需要修改或新增功能,在 HTML 标签内嵌入的大量代码将变得混乱不堪,难以管理。而 addEventListener 将事件绑定逻辑统一放置在 JavaScript 脚本中,实现了 HTML 结构与 JavaScript 行为的分离,遵循了良好的代码解耦原则,使得代码结构更加清晰、易于维护与拓展。在实际应用场景中,比如一个网页表单包含多个按钮,分别用于提交表单、重置表单、保存草稿等不同操作,便可利用 addEventListener 为每个按钮绑定各自专属的点击处理函数:<form id="myForm">    <input type="text" placeholder="请输入内容">    <button id="submitButton">提交</button>    <button id="resetButton">重置</button>    <button id="saveButton">保存草稿</button></form><script>    const submitBtn = document.getElementById('submitButton');    const resetBtn = document.getElementById('resetButton');    const saveBtn = document.getElementById('saveButton');     submitBtn.addEventListener('click', function() {        // 执行表单提交逻辑,如校验数据、发送请求        console.log('表单提交中...');    });     resetBtn.addEventListener('click', function() {        // 重置表单数据,恢复初始状态        console.log('表单已重置');    });     saveBtn.addEventListener('click', function() {        // 保存草稿逻辑,如将数据暂存本地        console.log('草稿已保存');    });</script>如此一来,各个按钮各司其职,通过独立的事件监听器实现了多样化的交互响应,极大提升了用户体验与代码的灵活性。五、实战案例:打造简易待办事项列表5.1 HTML 结构搭建在着手构建简易待办事项列表时,精心搭建 HTML 结构是基础且关键的第一步。它犹如搭建房屋的骨架,为后续功能的添砖加瓦提供稳固支撑。以下是一段基础的 HTML 代码示例:<!DOCTYPE html><html lang="en"> <head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>简易待办事项列表</title>    <link rel="stylesheet" href="styles.css"></head> <body>    <h1>待办事项列表</h1>    <div id="todo-app">        <input type="text" id="new-todo" placeholder="添加新任务...">        <button id="add-todo">添加</button>        <ul id="todo-list">        </ul>    </div>    <script src="script.js"></script></body> </html>在这段代码中,<h1> 标签醒目地展示了应用的标题,让用户一眼便能知晓此页面的用途。<div id="todo-app"> 作为核心容器,将整个待办事项的操作区域进行了整合,使之在页面布局上更为规整。其中,<input type="text" id="new-todo"> 是用户输入待办任务的入口,placeholder 属性友好地提示用户应在此处输入内容;<button id="add-todo"> 则是触发添加任务动作的按钮,简洁直观。而 <ul id="todo-list"> 如同一个空白的任务收纳盒,后续通过 JavaScript 动态生成的任务列表项(<li> 元素)都将被有序地放置其中,为用户呈现清晰的任务清单。合理且简洁的 HTML 结构设计,不仅提升了代码的可读性,更为后续 JavaScript 功能的实现铺就了顺畅之路。5.2 JavaScript 功能实现有了 HTML 结构作为基石,接下来借助 JavaScript 赋予待办事项列表鲜活的交互能力。以下是实现添加任务、删除任务以及标记任务完成功能的 JavaScript 代码示例,并附有详细注释:// 等待页面DOM加载完成后再执行后续代码,确保DOM元素已存在document.addEventListener('DOMContentLoaded', function () {    const todoInput = document.getElementById('new-todo');    const addButton = document.getElementById('add-todo');    const todoList = document.getElementById('todo-list');     // 为添加按钮添加点击事件监听器    addButton.addEventListener('click', function () {        const taskText = todoInput.value.trim();        if (taskText!== '') {            // 创建新的列表项元素            const listItem = document.createElement('li');            listItem.textContent = taskText;             // 创建删除按钮            const deleteButton = document.createElement('button');            deleteButton.textContent = '删除';            // 为删除按钮添加点击事件监听器,点击时移除对应的列表项            deleteButton.addEventListener('click', function () {                todoList.removeChild(listItem);            });             // 将删除按钮添加到列表项中            listItem.appendChild(deleteButton);             // 将新的列表项添加到待办事项列表            todoList.appendChild(listItem);             // 清空输入框,以便用户输入下一个任务            todoInput.value = '';        }    });     // 为待办事项列表添加点击事件监听器,用于标记任务完成    todoList.addEventListener('click', function (e) {        if (e.target.tagName === 'LI') {            e.target.classList.toggle('completed');        }    });});在上述代码中,首先使用 document.addEventListener('DOMContentLoaded', function () {...}) 确保整个 HTML 页面的 DOM 结构加载完毕后,才开始执行后续的 JavaScript 代码。这一步骤至关重要,因为若过早执行,可能会因 DOM 元素未完全加载而导致无法获取到相应元素,引发错误。接着,通过 document.getElementById 精准获取到输入框、添加按钮以及待办事项列表的 DOM 元素引用,并分别存储在 todoInput、addButton 和 todoList 变量中,方便后续操作。当用户在输入框输入任务文本并点击添加按钮时,addButton.addEventListener('click', function () {...}) 中的代码被触发。首先,获取输入框中的文本并去除首尾空格,若文本不为空,则开启创建新任务列表项的流程。使用 document.createElement('li') 生成一个新的 <li> 元素,并将输入的任务文本赋值给它的 textContent 属性,使其显示在页面上。同时,创建一个用于删除任务的按钮,同样为其添加点击事件监听器,当点击删除按钮时,执行 todoList.removeChild(listItem),直接从 DOM 树中移除对应的列表项,实现任务删除功能。最后,将新创建的列表项添加到待办事项列表中,并清空输入框,等待用户输入下一个任务。为了实现标记任务完成的功能,利用 todoList.addEventListener('click', function (e) {...}) 为整个待办事项列表添加点击事件监听器。当用户点击列表中的某个任务项(<li> 元素)时,通过判断 e.target.tagName === 'LI',确认点击的是任务项本身,随后使用 e.target.classList.toggle('completed'),动态切换任务项的 completed 类名。在 CSS 样式表中,可预先定义 .completed 类的样式,如添加删除线、改变字体颜色等,以此直观地呈现任务的完成状态,为用户提供清晰的视觉反馈,让待办事项管理更加便捷高效。六、进阶拓展:异步编程与 Ajax6.1 异步编程概念在 JavaScript 的编程世界里,同步与异步犹如两条截然不同的执行路径,深刻影响着程序的运行逻辑与用户体验。同步编程,恰似一位按部就班的 “执行者”,每一行代码都必须严格遵循顺序依次执行,犹如工厂流水线上的一道道工序,前一个任务未完成,后续任务只能默默等待。以读取本地文件为例,若使用同步方式,程序会在发出读取指令后,如同被定格一般,死死 “卡住”,直至文件完整读取并返回结果,才肯继续执行下一行代码。这种 “死等” 模式,在处理耗时较短的任务时,或许尚可接受;但一旦遭遇如大规模数据读取、复杂网络请求等耗时漫长的操作,问题便会接踵而至。整个程序仿佛陷入泥沼,界面冻结,用户的任何操作都得不到即时响应,极大地损害了用户体验。而异步编程,则像是一位高效的 “多面手”,当遇到诸如网络请求、文件读取这类耗时操作时,它不会傻傻等待,而是迅速开启新的任务分支,将后续代码的执行权交予主线程,让程序得以继续流畅运行。以网页加载图片为例,当浏览器发起图片加载请求后,并不会停滞不前,而是立即着手处理其他页面元素的渲染、脚本的执行等任务。待图片数据从服务器慢悠悠地传输回来,再由专门的回调函数或异步处理机制,将图片巧妙地安置到对应的位置。如此一来,用户便能在图片加载的间隙,正常进行页面滚动、点击链接等操作,页面始终保持着鲜活的响应能力,极大提升了交互的流畅性。再看一个从服务器获取数据来更新页面内容的场景。在同步模式下,页面会在数据请求发出后陷入僵局,直到数据完整抵达,才一次性更新页面,这期间用户面对的是毫无变化的 “白板”,极易产生焦虑与不耐烦。而采用异步编程,数据请求悄然在后台运作,主线程继续渲染页面骨架、设置基础样式,待数据到手,再通过 DOM 操作逐步、动态地填充内容,用户看到的是一个逐步鲜活起来的页面,交互体验天差地别。异步编程的核心优势,就在于巧妙地利用等待时间,让程序的各个部分并行推进,避免因个别耗时操作拖垮整个系统的响应速度,为用户带来丝滑流畅的浏览与操作体验。6.2 Ajax 原理与使用Ajax(Asynchronous JavaScript and XML),作为前端开发领域的一项关键技术,犹如一座隐形的桥梁,无缝连接着前端页面与后端服务器,实现了数据的异步交互与页面的局部更新,为用户带来流畅、高效的浏览体验。其核心原理依托于 XMLHttpRequest 对象(在现代浏览器中,也常使用更为简洁的 Fetch API),这一对象恰似一位 “幕后信使”,能够在不刷新整个页面的前提下,悄然向服务器发送 HTTP 请求,并机智地接收、处理服务器返回的数据。以一个常见的网页场景为例,当用户在搜索框输入关键词,期望实时获取搜索建议时,Ajax 便开始大展身手。使用 XMLHttpRequest 对象发起请求,代码大致如下:const xhr = new XMLHttpRequest();xhr.open('GET', 'https://example.com/api/search?q=' + encodeURIComponent(keyword), true);xhr.onreadystatechange = function() {    if (xhr.readyState === 4 && xhr.status === 200) {        const response = JSON.parse(xhr.responseText);        // 处理搜索建议数据,如更新下拉列表展示    }};xhr.send();这里,首先创建了 XMLHttpRequest 实例,通过 open 方法精心配置请求的类型(GET)、目标 URL(包含搜索关键词)以及异步模式(true)。接着,为 onreadystatechange 事件绑定回调函数,宛如设置了一个敏锐的 “瞭望哨”,时刻紧盯请求状态的变化。当 readyState 达到 4(意味着请求已完成,数据接收完毕)且状态码为 200(表示请求成功)时,便迅速对返回的 JSON 数据进行解析,并依据数据内容更新页面的搜索建议区域,整个过程页面纹丝不动,用户却能实时获取反馈。Fetch API 则以一种更加现代化、简洁的语法实现类似功能:fetch('https://example.com/api/search?q=' + encodeURIComponent(keyword))  .then(response => response.json())  .then(data => {        // 处理搜索建议数据    })  .catch(error => {        console.error('搜索请求出错:', error);    });Fetch API 采用链式调用的 Promise 风格,通过 then 方法依次处理请求成功后的响应解析、数据处理步骤,若途中出现错误,catch 方法便能精准捕获并处理,让异步数据交互的代码逻辑更加清晰、易读。不妨再看一个完整的示例,从服务器获取待办事项列表数据并实时更新页面展示:<!DOCTYPE html><html lang="en"> <head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Ajax待办事项示例</title>    <style>        #todo-list {            list-style-type: none;            padding: 0;        }         #todo-list li {            border: 1px solid #ccc;            margin: 5px;            padding: 5px;        }    </style></head> <body>    <h1>待办事项列表</h1>    <ul id="todo-list"></ul>    <script>        document.addEventListener('DOMContentLoaded', function () {            const todoList = document.getElementById('todo-list');            // 使用Fetch API获取待办事项数据            fetch('https://example.com/api/todos')              .then(response => response.json())              .then(todos => {                    todos.forEach(todo => {                        const listItem = document.createElement('li');                        listItem.textContent = todo.task;                        todoList.appendChild(listItem);                    });                })              .catch(error => {                    console.error('获取待办事项出错:', error);                });        });    </script></body> </html>在上述代码中,页面加载完成后,通过 Fetch API 向指定服务器接口发送请求,成功获取数据后,循环遍历待办事项数组,动态创建 DOM 元素并添加到页面列表中,瞬间为用户呈现出最新的待办任务清单,全程无刷新,交互体验流畅自然。Ajax 技术的应用,让网页告别了频繁整页刷新的笨拙,实现了数据与页面展示的精妙同步,极大提升了用户体验与应用的响应效率。七、前沿框架:Vue.js 入门窥探7.1 Vue.js 简介在当今蓬勃发展的前端开发领域,Vue.js 宛如一颗璀璨夺目的新星,以其卓越的特性迅速赢得了广大开发者的青睐,成为构建现代用户界面的得力工具。Vue.js 最为突出的优势之一便是其精妙绝伦的响应式数据绑定机制。传统的 JavaScript 开发模式下,当数据发生变化时,开发者需手动编写冗长繁杂的代码来精准定位并更新对应的 DOM 元素,这一过程极易出错且效率低下,犹如在错综复杂的迷宫中艰难寻路。而 Vue.js 通过其内部强大的响应式系统,能够自动 “感知” 数据的细微变化,宛如一位时刻警觉的守护者,一旦数据有所异动,便立即高效且智能地更新与之关联的 DOM 内容,确保视图与数据始终保持高度一致,实现无缝同步。组件化开发则是 Vue.js 的另一大 “杀手锏”。它倡导将复杂的用户界面拆解为一个个独立、可复用的小型组件,恰似将一座宏伟的大厦拆分为众多标准化的积木模块。每个组件都拥有自己独立的 HTML 模板、JavaScript 逻辑以及 CSS 样式,它们既能在不同场景下被重复调用,又能依据需求灵活组合,极大地提升了开发效率与代码的可维护性。例如,在构建一个大型电商网站时,头部导航栏、商品列表、购物车等功能模块均可封装为独立组件,开发团队可并行推进各组件的开发,后续若需优化某个组件,也只需聚焦于该组件内部代码,避免牵一发而动全身,让项目开发与维护变得井井有条。对比原生 JavaScript 开发,Vue.js 的高效性体现得淋漓尽致。以构建一个具有动态数据展示与交互功能的页面为例,原生 JavaScript 需要耗费大量精力处理 DOM 操作、事件绑定以及数据更新的复杂逻辑,代码往往冗长且晦涩难懂,如同杂乱无章的线缆交织在一起;而 Vue.js 凭借简洁优雅的模板语法、高效的数据绑定以及组件化架构,能够以更少的代码量、更清晰的逻辑结构实现同样甚至更为强大的功能,宛如一位技艺精湛的魔法师,用简洁的咒语变出绚丽的魔法,让开发者从繁琐的底层操作中解脱出来,将更多精力投入到业务逻辑与用户体验的优化上,快速打造出高性能、交互性强的优质网页应用。7.2 基础使用示例下面通过一个简单的计数器示例,来初步领略 Vue.js 的魅力与便捷。首先,引入 Vue.js 库。可以通过在 HTML 页面的<head>标签内使用<script>标签引入 CDN 链接,如下:<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>接着,在 HTML 的<body>标签内创建一个 DOM 元素作为 Vue 实例的挂载点,例如:<div id="app"></div>然后,编写 JavaScript 代码创建 Vue 实例并进行数据绑定:var app = new Vue({    el: '#app',    data: {        count: 0    }});这里,el属性指定了 Vue 实例挂载的 DOM 元素选择器,data对象则包含了应用所需的数据,此处仅有一个count属性,初始值为 0。在 HTML 中,使用 Vue.js 的模板语法将数据绑定到 DOM 元素上:<div id="app">    <p>当前计数:{    { count }}</p>    <button @click="count++">点击增加</button></div>在上述代码中,双大括号{ { count }}便是 Vue.js 的文本插值语法,它能够实时将count的数据渲染到 DOM 中,让用户直观看到当前计数。而@click="count++"则是 Vue.js 的事件绑定语法,它监听按钮的点击事件,每次点击时,count的值便会自动加 1,由于 Vue.js 的响应式特性,与之绑定的 DOM 内容也会瞬间更新,完美展现数据双向绑定的效果。不妨设想一下,如果使用原生 JavaScript 来实现相同功能,需要手动获取 DOM 元素、监听按钮点击事件、更新数据并操作 DOM 来修改显示文本,代码复杂度大幅提升,且易出现诸如事件绑定错误、DOM 更新不及时等问题。而 Vue.js 通过简洁的语法糖,将复杂的交互逻辑封装得优雅而高效,让开发者能轻松构建动态交互界面,开启便捷开发之旅。八、总结与展望至此,我们已一同穿越了 JavaScript 前端开发的核心地带,领略了其从基础语法、DOM 操作、事件处理,到实战应用、异步编程以及前沿框架 Vue.js 入门的独特魅力。在这个过程中,我们明晰了变量与数据类型的精妙差异,熟练掌握了条件循环语句与函数的灵活运用,学会了运用 DOM 操作精准掌控网页元素,巧用事件处理搭建起用户与网页交互的坚实桥梁,通过实战打造出实用的待办事项列表,深入理解异步编程提升页面性能,并初探 Vue.js 感受现代框架的高效便捷。然而,JavaScript 的世界广袤无垠,始终处于蓬勃发展之中。新的特性、框架、工具如繁星般不断涌现。作为前端开发者,持续学习是我们前行的不二法则。需时刻关注 ECMAScript 的最新标准,探索如 React、Angular 等其他前沿框架的独特优势,深入钻研 WebAssembly、PWA 等新兴技术,将其巧妙融入项目,创造更为精彩卓越的用户交互体验。愿大家在 JavaScript 前端开发的征程中,不断探索、砥砺奋进,书写属于自己的精彩篇章,让互联网世界因我们的代码而绽放更加绚烂的光彩。———————————————— 原文链接:https://blog.csdn.net/weixin_73295475/article/details/145325390
  • [技术干货] 深入理解 ThreadLocal 原理及其在 Java 多线程上下文管理中的应用
    引言在多线程编程中,线程间的数据共享与隔离是一个非常重要的话题。Java 提供了多种机制来处理多线程环境下的数据共享问题,其中 ThreadLocal 是一个非常有用的工具。ThreadLocal 允许我们为每个线程创建一个独立的变量副本,从而避免线程间的数据竞争和同步问题。本文将深入探讨 ThreadLocal 的工作原理,并通过代码示例展示如何在 Java 多线程环境中使用 ThreadLocal 进行上下文管理。一、ThreadLocal 的基本概念1.1 什么是 ThreadLocal?ThreadLocal 是 Java 提供的一个线程级别的变量存储类。它为每个使用该变量的线程提供了一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。ThreadLocal 通常用于在多线程环境中保存线程的上下文信息,如用户会话、数据库连接等。1.2 ThreadLocal 的使用场景线程上下文管理:在多线程环境中,ThreadLocal 可以用于保存线程的上下文信息,如用户会话、事务 ID 等。避免参数传递:在某些情况下,ThreadLocal 可以避免在方法调用链中传递参数,简化代码。线程安全的对象管理:ThreadLocal 可以用于管理线程安全的对象,如 SimpleDateFormat 等。二、ThreadLocal 的工作原理2.1 ThreadLocal 的内部结构ThreadLocal 的核心思想是为每个线程维护一个独立的变量副本。为了实现这一点,ThreadLocal 内部使用了一个名为 ThreadLocalMap 的静态内部类。ThreadLocalMap 是一个定制化的哈希表,用于存储线程的变量副本。每个 Thread 对象内部都有一个 ThreadLocalMap 的实例,用于存储该线程的所有 ThreadLocal 变量。当调用 ThreadLocal 的 get() 或 set() 方法时,ThreadLocal 会首先获取当前线程的 ThreadLocalMap,然后在该 ThreadLocalMap 中进行操作。2.2 ThreadLocal 的核心方法set(T value):将当前线程的 ThreadLocal 变量副本设置为指定的值。get():返回当前线程的 ThreadLocal 变量副本。remove():移除当前线程的 ThreadLocal 变量副本。2.3 ThreadLocalMap 的实现ThreadLocalMap 是一个定制化的哈希表,它的键是 ThreadLocal 对象,值是对应的变量副本。ThreadLocalMap 使用开放地址法来解决哈希冲突,即当发生冲突时,它会寻找下一个空闲的槽位来存储数据。ThreadLocalMap 的键是弱引用(WeakReference),这意味着当 ThreadLocal 对象不再被引用时,它会被垃圾回收器回收,从而避免内存泄漏。三、ThreadLocal 的使用示例3.1 基本使用下面是一个简单的示例,展示了如何使用 ThreadLocal 来保存线程的上下文信息。public class ThreadLocalExample {    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();    public static void main(String[] args) {        Runnable task = () -> {            // 设置线程的上下文信息            threadLocal.set(Thread.currentThread().getName() + " - context");            // 获取线程的上下文信息            System.out.println(threadLocal.get());            // 清除线程的上下文信息            threadLocal.remove();        };        // 创建多个线程并启动        Thread thread1 = new Thread(task);        Thread thread2 = new Thread(task);        thread1.start();        thread2.start();    }}在这个示例中,我们创建了一个 ThreadLocal 变量 threadLocal,并在每个线程中设置和获取该变量的值。由于 ThreadLocal 为每个线程提供了独立的变量副本,因此每个线程都可以安全地访问和修改自己的副本,而不会影响其他线程。3.2 使用 ThreadLocal 管理线程安全的对象ThreadLocal 还可以用于管理线程安全的对象,如 SimpleDateFormat。由于 SimpleDateFormat 不是线程安全的,因此在多线程环境中使用时需要进行同步。通过 ThreadLocal,我们可以为每个线程创建一个独立的 SimpleDateFormat 实例,从而避免同步开销。public class ThreadLocalDateFormat {    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));    public static String formatDate(Date date) {        return dateFormatThreadLocal.get().format(date);    }    public static void main(String[] args) {        Runnable task = () -> {            String formattedDate = formatDate(new Date());            System.out.println(Thread.currentThread().getName() + " - " + formattedDate);        };        // 创建多个线程并启动        Thread thread1 = new Thread(task);        Thread thread2 = new Thread(task);        thread1.start();        thread2.start();    }}在这个示例中,我们使用 ThreadLocal 为每个线程创建了一个独立的 SimpleDateFormat 实例。这样,每个线程都可以安全地使用自己的 SimpleDateFormat 实例,而无需担心线程安全问题。四、ThreadLocal 的内存泄漏问题4.1 内存泄漏的原因虽然 ThreadLocal 提供了线程级别的变量隔离,但如果使用不当,可能会导致内存泄漏问题。ThreadLocalMap 中的键是弱引用,这意味着当 ThreadLocal 对象不再被引用时,它会被垃圾回收器回收。然而,ThreadLocalMap 中的值仍然是强引用,因此如果 ThreadLocal 对象被回收,但 ThreadLocalMap 中的值没有被清除,就会导致内存泄漏。4.2 如何避免内存泄漏为了避免内存泄漏,我们应该在使用完 ThreadLocal 变量后,调用 remove() 方法将其从 ThreadLocalMap 中移除。这样可以确保 ThreadLocalMap 中的值不会一直保留,从而避免内存泄漏。public class ThreadLocalMemoryLeakExample {    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();    public static void main(String[] args) {        Runnable task = () -> {            try {                // 设置线程的上下文信息                threadLocal.set(Thread.currentThread().getName() + " - context");                // 模拟业务逻辑                System.out.println(threadLocal.get());            } finally {                // 清除线程的上下文信息                threadLocal.remove();            }        };        // 创建多个线程并启动        Thread thread1 = new Thread(task);        Thread thread2 = new Thread(task);        thread1.start();        thread2.start();    }}在这个示例中,我们在 finally 块中调用了 threadLocal.remove() 方法,以确保在使用完 ThreadLocal 变量后将其清除,从而避免内存泄漏。五、总结ThreadLocal 是 Java 多线程编程中一个非常有用的工具,它允许我们为每个线程创建一个独立的变量副本,从而避免线程间的数据竞争和同步问题。通过 ThreadLocal,我们可以轻松地管理线程的上下文信息,简化代码,并提高程序的性能。然而,ThreadLocal 也存在内存泄漏的风险,因此在使用时需要注意及时清理不再需要的变量副本。通过合理地使用 ThreadLocal,我们可以在多线程环境中更好地管理线程的上下文信息,提高程序的稳定性和可维护性。希望本文能够帮助你更好地理解 ThreadLocal 的工作原理,并在实际开发中灵活运用它来解决多线程环境下的数据共享与隔离问题————————————————原文链接:https://blog.csdn.net/weixin_44976692/article/details/145410897
  • [技术干货] Java 枚举
    枚举是什么枚举(enum):是一种特殊的类,用于定义一组常量,将其组织起来。枚举使得代码更具有可读性和可维护性,特别是在处理固定集合的值时,如:星期、月份、状态码等在 Java 中,使用关键字 enum 来定义枚举类:public enum Day {    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;}其中,定义的枚举项就是该类的实例,且必须在第一行,最后一个枚举项后的分号; 可以省略,但是若枚举类有其他内容,则分号不能省略(最好不要省略) 当类初始化时,这些枚举项就会被实例化枚举类使用 enum 定义后,默认继承 java.lang.Enum 类,也就是说,我们自己写的枚举类,就算没有显示的继承 Enum,但是其默认继承了这个类此外,枚举在 Java 中不能被继承,自定义的枚举类隐式继承自 java.lang.Enum 类,且不能再继承其他类,这样的设计确保了枚举类的简单性和一致性。如果枚举可以继承其他类,将会导致复杂的继承关系,并且影响Java的类型系统常用方法方法    描述values()    以数组的形式返回枚举类型的所有成员ordinal()获取枚举成员的索引位置valueOf()    将普通字符串转换为枚举实例compareTo(E o)比较两个枚举成员在定义时的顺序 我们通过一个示例,来学习和使用这些方法:public enum Day {    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;     public static void main(String[] args) {        // 获取所有枚举成员        Day[] days = Day.values();        // 遍历        for (int i = 0; i < days.length; i++) {            // 获取枚举成员以及索引位置            System.out.println(days[i] + " " + days[i].ordinal());        }        // 将普通字符串转换为枚举实例        System.out.println(Day.valueOf("THURSDAY"));        // 获取枚举实例 SUNDAY 和 SATURDAY        Day sunday = Day.SUNDAY;        Day saturday = Day.SATURDAY;        // 比较定义时的顺序        System.out.println(sunday.compareTo(saturday));     }}运行结果: 在使用 valueOf() 方法进行转换时,传递的名称必须与枚举常量的名字完全匹配(包括大小写),若不匹配,就会抛出 IllegalArgumentException 异常:  当我们查看 java.lang.Enum 时: 可以看到,valueOf() 方法包含了两个类型的参数:Class<T> enumClass  和  String name其中Class<T> enumType 是一个 Class 对象,表示要查找的枚举类型String name: 是一个字符串,表示要查找的枚举常量的名称。名称必须与枚举常量的名字完全匹配(包括大小写) 但是在使用时,我们只传递了一个参数 name,也能够进行转换,这是为什么呢?这是因为,在 Java 中,valueOf 方法实际上是自动生成的,属于每个枚举类型的特性。虽然它的原始定义需要两个参数(类类型和名称),但是每个枚举类型都会自动提供一个与自身类型相关联的 valueOf 方法,只需传递一个字符串参数因此,当我们调用 Day.valueOf("THURSDAY") 时,Java会自动处理这个调用,实际调用的是包含类名的 valueOf 方法,而不是原始的静态方法定义再观察 java.lang.Enum: 我们会发现,其中并不存在 values() 这个方法,而当我们点击 values() 方法时,则会跳转到本类上那么,values() 方法是从哪来的呢?values() 方法是枚举类自动提供的一个静态方法,允许我们获取一个包含所有枚举常量的数组,这个方法是由Java编译器自动生成的,在编译时每个枚举类型都会自动生成一个 values() 方法,因此不需要我们显式定义它我们将枚举类进行反编译:(1)打开 cmd,切换到 Day.java 文件所在目录(2)编译 .java 文件(javac Day.java)(3)将 .class 文件进行反编译(javap -c Day.class > day.txt)打开 day.txt,可以看到: 编译器自动为我们生成了 values 和 valueOf 方法构造方法当我们创建构造方法时:  不能使用 public 来修饰构造方法,为什么呢?这是因为,在 Java 中,枚举类的构造方法都是私有的(不加任何修饰符时,默认是 private),无法在枚举类外部调用,这也就防止了在枚举类外部创建新的枚举常量在定义枚举常量时,构造方法会被隐式调用:public enum Day {    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;     Day() {        System.out.println("构造方法");    }     public static void main(String[] args) {        System.out.println("------------------");    }}运行结果:   当我们定义带有参数的构造方法时,在创建枚举项时,也要为其提供对应参数:public enum Day {    SUNDAY("周天", 7),    MONDAY("周一", 1),    TUESDAY("周二", 2),    WEDNESDAY("周三", 3),    THURSDAY("周四", 4),    FRIDAY("周五", 5),    SATURDAY("周六", 6);    private String name;    private int key;     Day(String name, int key) {        this.name = name;        this.key = key;    }}枚举的优缺点优点:(1)枚举常量确保了只能使用定义的常量,避免了使用整型常量时可能带来的错误(2)使用枚举可以使代码更易读,表达清晰(3)枚举定义了一组固定的常量,适合表示有限的状态或选项,便于管理和维护(4)枚举可以拥有字段、方法和构造方法,能够封装与常量相关的行为和属性(5)Java的枚举类自带一些方法,如 values()、valueOf() 等(6)可以用于 switch 语句缺点:(1)枚举的集合是固定的,无法在运行时添加或删除常量。如果需要动态的集合,枚举可能不适用(2)枚举不能继承,无法扩展(3)每个枚举常量都是一个对象,可能会增加内存使用,特别是当枚举常量数量较多时枚举和反射在 Java 反射-CSDN博客 中,我们学习了反射,通过反射,我们可以拿到类的私有构造方法,从而创建实例对象那么,枚举是否可以通过反射,拿到实例对象呢?public enum Day {    SUNDAY("周天", 7),    MONDAY("周一", 1),    TUESDAY("周二", 2),    WEDNESDAY("周三", 3),    THURSDAY("周四", 4),    FRIDAY("周五", 5),    SATURDAY("周六", 6);    private String name;    private int key;     Day(String name, int key) {        this.name = name;        this.key = key;    }}public class Test {    public static void main(String[] args) {        Class<?> classDay = null;        try {            classDay = Class.forName("Day");            Constructor<?> constructor = classDay.getDeclaredConstructor(String.class, int.class);            constructor.setAccessible(true);            Day day = (Day) constructor.newInstance("sunday", 0);            System.out.println(day.ordinal());        } catch (ClassNotFoundException e) {            throw new RuntimeException(e);        } catch (NoSuchMethodException e) {            throw new RuntimeException(e);        } catch (InvocationTargetException e) {            throw new RuntimeException(e);        } catch (InstantiationException e) {            throw new RuntimeException(e);        } catch (IllegalAccessException e) {            throw new RuntimeException(e);        }    }}运行结果: 此时程序抛出了 NoSuchMethodException 异常,也就是没有对应的构造方法但是提供的枚举的构造方法就是带有两个参数,分别为 String 和 int: 那么,问题出在哪里呢?自定义的枚举类默认继承自 java.lang.Enum因此,自定义的枚举类继承了父类除构造方法外的所有东西,且子类需要帮助父类进行构造,但我们实现的类中,并没有帮助父类进行构造因此,我们需要在枚举类中帮助父类进行构造,而父类中的构造方法为: 那么,如何实现呢?通过 super 方法吗?但是,当我们在构造方法中调用 super 时: 枚举构造方法中不能使用 super 由于枚举比较特殊,在构造方法中,除了我们自定义了两个参数,它还默认添加了父类的两个参数也就是说,构造函数中一共有四个参数:String int String int其中,前两个参数是父类参数,后两个参数是子类参数public class Test {    public static void main(String[] args) {        Class<?> classDay = null;        try {            classDay = Class.forName("enumDemo.Day");            Constructor<?> constructor = classDay.getDeclaredConstructor(String.class, int.class, String.class, int.class);            constructor.setAccessible(true);            Day day = (Day) constructor.newInstance("父类参数", 0, "子类参数", 0);            System.out.println(day.ordinal());        } catch (ClassNotFoundException e) {            throw new RuntimeException(e);        } catch (NoSuchMethodException e) {            throw new RuntimeException(e);        } catch (InvocationTargetException e) {            throw new RuntimeException(e);        } catch (InstantiationException e) {            throw new RuntimeException(e);        } catch (IllegalAccessException e) {            throw new RuntimeException(e);        }    }}再次运行: 此时抛出了 IllegalArgumentException 异常,不能通过反射创建枚举对象枚举保证了每个枚举常量只有一个实例,这种唯一性在枚举类型被定义时就已经确定,不允许外部创建新的实例,枚举类型的设计使得它们的实例在类加载时被唯一地定义,从而避免了通过反射创建新的枚举实例的可能性,确保了枚举的强类型安全性和唯一性实现单例模式在 单例模式:饿汉模式、懒汉模式_单例模式懒汉和饿汉-CSDN博客 中我们实现了单例模式,单例模式能够确保一个类只有一个实例但普通类可以通过反射机制打破,因此,我们可以使用枚举来实现单例模式public enum Singleton {    INSTANCE;    public Singleton getInstance() {        return INSTANCE;    }————————————————                    原文链接:https://blog.csdn.net/2301_76161469/article/details/143241972
  • [技术干货] 【Java从入门到起飞】流程控制语句
    1. 顺序结构顺序结构就是程序从上到下逐行地执行。表达式语句都是顺序执行的。并且上一行对某个变量的修改对下一行会产生影响。 public class StatementTest{public static void main(String[] args){int x = 1;int y = 2;System.out.println("x = " + x);        System.out.println("y = " + y);        //对x、y的值进行修改        x++;        y = 2 * x + y;        x = x * 10;        System.out.println("x = " + x);        System.out.println("y = " + y);    }} Java中定义变量时采用合法的前向引用。如: public static void main(String[] args) {int num1 = 12;int num2 = num1 + 2;} 错误形式: public static void main(String[] args) {int num2 = num1 + 2;int num1 = 12;} 2. 分支语句2.1 if-else条件判断结构2.1.1 基本语法结构1:单分支条件判断:if 格式: if(条件表达式){  语句块;} 说明:条件表达式必须是布尔表达式(关系表达式或逻辑表达式)或 布尔变量。 执行流程: 首先判断条件表达式看其结果是true还是false如果是true就执行语句块如果是false就不执行语句块  结构2:双分支条件判断:if…else 格式: if(条件表达式) {   语句块1;}else {  语句块2;} 执行流程: 首先判断条件表达式看其结果是true还是false如果是true就执行语句块1如果是false就执行语句块2  结构3:多分支条件判断:if…else if…else 格式: if (条件表达式1) {  语句块1;} else if (条件表达式2) {  语句块2;}...}else if (条件表达式n) {  语句块n;} else {  语句块n+1;} 说明:一旦条件表达式为true,则进入执行相应的语句块。执行完对应的语句块之后,就跳出当前结构。 执行流程: 首先判断关系表达式1看其结果是true还是false如果是true就执行语句块1,然后结束当前多分支如果是false就继续判断关系表达式2看其结果是true还是false如果是true就执行语句块2,然后结束当前多分支如果是false就继续判断关系表达式…看其结果是true还是false​ … n. 如果没有任何关系表达式为true,就执行语句块n+1,然后结束当前多分支。   当条件表达式之间是“互斥”关系时(即彼此没有交集),条件判断语句及执行语句间顺序无所谓。 当条件表达式之间是“包含”关系时,“小上大下 / 子上父下”,否则范围小的条件表达式将不可能被执行。 2.1.3 if…else嵌套在 if 的语句块中,或者是在else语句块中,又包含了另外一个条件判断(可以是单分支、双分支、多分支),就构成了嵌套结构。 执行的特点:(1)如果是嵌套在if语句块中的,只有当外部的if条件满足,才会去判断内部的条件(2)如果是嵌套在else语句块中的,只有当外部的if条件不满足,进入else后,才会去判断内部的条件 **案例4:**由键盘输入三个整数分别存入变量num1、num2、num3,对它们进行排序(使用 if-else if-else),并且从小到大输出。 class IfElseTest4 {public static void main(String[] args) { //声明num1,num2,num3三个变量并赋值int num1 = 23,num2 = 32,num3 = 12; if(num1 >= num2){ if(num3 >= num1)System.out.println(num2 + "-" + num1 + "-" + num3);else if(num3 <= num2)System.out.println(num3 + "-" + num2 + "-" + num1);elseSystem.out.println(num2 + "-" + num3 + "-" + num1);}else{ //num1 < num2 if(num3 >= num2){System.out.println(num1 + "-" + num2 + "-" + num3);}else if(num3 <= num1){System.out.println(num3 + "-" + num1 + "-" + num2);}else{System.out.println(num1 + "-" + num3 + "-" + num2);}}}} 2.1.4 其它说明语句块只有一条执行语句时,一对{}可以省略,但建议保留当if-else结构是“多选一”时,最后的else是可选的,根据需要可以省略2.2 switch-case选择结构2.2.1 基本语法语法格式: switch(表达式){    case 常量值1:        语句块1;        //break;    case 常量值2:        语句块2;        //break;     // ...   [default:        语句块n+1;        break;   ]} 执行过程: 第1步:根据switch中表达式的值,依次匹配各个case。如果表达式的值等于某个case中的常量值,则执行对应case中的执行语句。 第2步:执行完此case的执行语句以后,​ 情况1:如果遇到break,则执行break并跳出当前的switch-case结构​ 情况2:如果没有遇到break,则会继续执行当前case之后的其它case中的执行语句。—>case穿透​ …​ 直到遇到break关键字或执行完所有的case及default的执行语句,跳出当前的switch-case结构 使用注意点: switch(表达式)中表达式的值必须是下述几种类型之一:byte,short,char,int,枚举 (jdk 5.0),String (jdk 7.0); case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围; 同一个switch语句,所有case子句中的常量值互不相同; break语句用来在执行完一个case分支后使程序跳出switch语句块; 如果没有break,程序会顺序执行到switch结尾; default子句是可选的。同时,位置也是灵活的。当没有匹配的case时,执行default语句。 2.2.3 利用case的穿透性在switch语句中,如果case的后面不写break,将出现穿透现象,也就是一旦匹配成功,不会在判断下一个case的值,直接向后运行,直到遇到break或者整个switch语句结束,执行终止。 案例:编写程序:从键盘上输入2023年的“month”和“day”,要求通过程序输出输入的日期为2023年的第几天。 import java.util.Scanner; class SwitchCaseTest4 {public static void main(String[] args) { Scanner scan = new Scanner(System.in); System.out.println("请输入2023年的month:");int month = scan.nextInt(); System.out.println("请输入2023年的day:");int day = scan.nextInt(); //这里就不针对month和day进行合法性的判断了,以后可以使用正则表达式进行校验。 int sumDays = 0;//记录总天数 //写法1 :不推荐(存在冗余的数据)/*switch(month){case 1:sumDays = day;break;case 2:sumDays = 31 + day;break;case 3:sumDays = 31 + 28 + day;break;//.... case 12://sumDays = 31 + 28 + ... + 30 + day;break;}*/ //写法2:推荐switch(month){case 12:sumDays += 30;//这个30是代表11月份的满月天数case 11:sumDays += 31;//这个31是代表10月份的满月天数case 10:sumDays += 30;//这个30是代表9月份的满月天数case 9:sumDays += 31;//这个31是代表8月份的满月天数case 8:sumDays += 31;//这个31是代表7月份的满月天数case 7:sumDays += 30;//这个30是代表6月份的满月天数case 6:sumDays += 31;//这个31是代表5月份的满月天数case 5:sumDays += 30;//这个30是代表4月份的满月天数case 4:sumDays += 31;//这个31是代表3月份的满月天数case 3:sumDays += 28;//这个28是代表2月份的满月天数case 2:sumDays += 31;//这个31是代表1月份的满月天数case 1:sumDays += day;//这个day是代表当月的第几天} System.out.println(month + "月" + day + "日是2023年的第" + sumDays + "天");        //关闭资源scan.close();}} 2.2.4 if-else语句与switch-case语句比较结论:凡是使用switch-case的结构都可以转换为if-else结构。反之,不成立。 开发经验:如果既可以使用switch-case,又可以使用if-else,建议使用switch-case。因为效率稍高。 细节对比: if-else语句优势if语句的条件是一个布尔类型值,if条件表达式为true则进入分支,可以用于范围的判断,也可以用于等值的判断,使用范围更广。switch语句的条件是一个常量值(byte,short,int,char,枚举,String),只能判断某个变量或表达式的结果是否等于某个常量值,使用场景较狭窄。switch语句优势当条件是判断某个变量或表达式是否等于某个固定的常量值时,使用if和switch都可以,习惯上使用switch更多。因为效率稍高。当条件是区间范围的判断时,只能使用if语句。使用switch可以利用穿透性,同时执行多个分支,而if…else没有穿透性。3. 循环语句理解:循环语句具有在某些条件满足的情况下,反复执行特定代码的功能。 循环结构分类: for 循环while 循环do-while 循环循环结构四要素: 初始化部分循环条件部分循环体部分迭代部分3.1 for循环3.1.1 基本语法语法格式: for (①初始化部分; ②循环条件部分; ④迭代部分){          ③循环体部分;} **执行过程:**①-②-③-④-②-③-④-②-③-④-…-② 说明: for(;;)中的两个;不能多也不能少①初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔②循环条件部分为boolean类型表达式,当值为false时,退出循环④可以有多个变量更新,用逗号分隔说明: 1、我们可以在循环中使用break。一旦执行break,就跳出当前循环结构。 2、小结:如何结束一个循环结构? ​ 结束情况1:循环结构中的循环条件部分返回false ​ 结束情况2:循环结构中执行了break。 3、如果一个循环结构不能结束,那就是一个死循环!我们开发中要避免出现死循环。 3.2 while循环3.2.1 基本语法语法格式: ①初始化部分while(②循环条件部分){    ③循环体部分;    ④迭代部分;} **执行过程:**①-②-③-④-②-③-④-②-③-④-…-② 说明: while(循环条件)中循环条件必须是boolean类型。注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。for循环和while循环可以相互转换。二者没有性能上的差别。实际开发中,根据具体结构的情况,选择哪个格式更合适、美观。for循环与while循环的区别:初始化条件部分的作用域不同。3.3 do-while循环3.3.1 基本语法语法格式: ①初始化部分;do{③循环体部分④迭代部分}while(②循环条件部分);  **执行过程:**①-③-④-②-③-④-②-③-④-…-② 图示:  说明: 结尾while(循环条件)中循环条件必须是boolean类型do{}while();最后有一个分号do-while结构的循环体语句是至少会执行一次,这个和for和while是不一样的循环的三个结构for、while、do-while三者是可以相互转换的。3.4 对比三种循环结构三种循环结构都具有四个要素:循环变量的初始化条件循环条件循环体语句块循环变量的修改的迭代表达式从循环次数角度分析do-while循环至少执行一次循环体语句。for和while循环先判断循环条件语句是否成立,然后决定是否执行循环体。如何选择遍历有明显的循环次数(范围)的需求,选择for循环遍历没有明显的循环次数(范围)的需求,选择while循环如果循环体语句块至少执行一次,可以考虑使用do-while循环本质上:三种循环之间完全可以互相转换,都能实现循环的功能3.5 "无限"循环  3.5.1 基本语法语法格式: 最简单"无限"循环格式:while(true) , for(;;)适用场景: 开发中,有时并不确定需要循环多少次,需要根据循环体内部某些条件,来控制循环的结束(使用break)。如果此循环结构不能终止,则构成了死循环!开发中要避免出现死循环。3.6 嵌套循环(或多重循环)3.6.1 使用说明所谓嵌套循环,是指一个循环结构A的循环体是另一个循环结构B。比如,for循环里面还有一个for循环,就是嵌套循环。其中,for ,while ,do-while均可以作为外层循环或内层循环。外层循环:循环结构A内层循环:循环结构B实质上,嵌套循环就是把内层循环当成外层循环的循环体。只有当内层循环的循环条件为false时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的外层循环。设外层循环次数为m次,内层为n次,则内层循环体实际上需要执行m*n次。**技巧:**从二维图形的角度看,外层循环控制行数,内层循环控制列数。**开发经验:**实际开发中,我们最多见到的嵌套循环是两层。一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内。否则,可读性会很差。例如:两个for嵌套循环格式 for(初始化语句①; 循环条件语句②; 迭代语句⑦) {    for(初始化语句③; 循环条件语句④; 迭代语句⑥) {      循环体语句⑤;    }} //执行过程:① - ② - ③ - ④ - ⑤ - ⑥ - ④ - ⑤ - ⑥ - ... - ④ - ⑦ - ② - ③ - ④ - ⑤ - ⑥ - ④.. **执行特点:**外层循环执行一次,内层循环执行一轮。 4. 关键字break和continue的使用4.1 break和continue的说明适用范围 在循环结构中使用的作用 相同点 break switch-case循环结构 一旦执行,就结束(或跳出)当前循环结构     此关键字的后面,不能声明语句 continue 循环结构 一旦执行,就结束(或跳出)当次循环结构     此关键字的后面,不能声明语句 此外,很多语言都有goto语句,goto语句可以随意将控制转移到程序中的任意一条语句上,然后执行它,但使程序容易出错。Java中的break和continue是不同于goto的。 4.2 应用举例class BreakContinueTest1 {public static void main(String[] args) { for(int i = 1;i <= 10;i++){ if(i % 4 == 0){//break;//123continue;//123567910//如下的语句不可能被执行,编译不通过//System.out.println("今晚迪丽热巴要约我吃饭");} System.out.print(i);} System.out.println("####"); //嵌套循环中的使用for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){if(j % 4 == 0){//break; //结束的是包裹break关键字的最近的一层循环!continue;//结束的是包裹break关键字的最近的一层循环的当次!}System.out.print(j);}System.out.println();} }} 4.3 带标签的使用break语句用于终止某个语句块的执行{    ……  break;……} break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块 label1: {   ……        label2:      {   ……label3: {   ……           break label2;           ……}     }}  continue语句出现在多层嵌套的循环语句体中时,也可以通过标签指明要跳过的是哪一层循环。 标号语句必须紧接在循环的头部。标号语句不能用在非循环语句的前面。 举例: class BreakContinueTest2 {public static void main(String[] args) {l:for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){if(j % 4 == 0){//break l;continue l;}System.out.print(j);}System.out.println();}}} 5. Scanner:键盘输入功能的实现如何从键盘获取不同类型(基本数据类型、String类型)的变量:使用Scanner类。 键盘输入代码的四个步骤: 导包:import java.util.Scanner;创建Scanner类型的对象:Scanner scan = new Scanner(System.in);调用Scanner类的相关方法(next() / nextXxx()),来获取指定类型的变量释放资源:scan.close();注意:需要根据相应的方法,来输入指定类型的值。如果输入的数据类型与要求的类型不匹配时,会报异常 导致程序终止。 5.1 各种类型的数据输入**案例:**小明注册某交友网站,要求录入个人相关信息。如下: 请输入你的网名、你的年龄、你的体重、你是否单身、你的性别等情况。 //① 导包import java.util.Scanner; public class ScannerTest1 {     public static void main(String[] args) {        //② 创建Scanner的对象        //Scanner是一个引用数据类型,它的全名称是java.util.Scanner        //scanner就是一个引用数据类型的变量了,赋给它的值是一个对象(对象的概念我们后面学习,暂时先这么叫)        //new Scanner(System.in)是一个new表达式,该表达式的结果是一个对象        //引用数据类型  变量 = 对象;        //这个等式的意思可以理解为用一个引用数据类型的变量代表一个对象,所以这个变量的名称又称为对象名        //我们也把scanner变量叫做scanner对象        Scanner scanner = new Scanner(System.in);//System.in默认代表键盘输入                //③根据提示,调用Scanner的方法,获取不同类型的变量        System.out.println("欢迎光临你好我好交友网站!");        System.out.print("请输入你的网名:");        String name = scanner.next();         System.out.print("请输入你的年龄:");        int age = scanner.nextInt();         System.out.print("请输入你的体重:");        double weight = scanner.nextDouble();         System.out.print("你是否单身(true/false):");        boolean isSingle = scanner.nextBoolean();         System.out.print("请输入你的性别:");        char gender = scanner.next().charAt(0);//先按照字符串接收,然后再取字符串的第一个字符(下标为0)         System.out.println("你的基本情况如下:");        System.out.println("网名:" + name + "\n年龄:" + age + "\n体重:" + weight +                            "\n单身:" + isSingle + "\n性别:" + gender);                //④ 关闭资源        scanner.close();    }}  6. 如何获取一个随机数如何产生一个指定范围的随机整数? 1、Math类的random()的调用,会返回一个[0,1)范围的一个double型值 2、Math.random() * 100 —> [0,100)(int)(Math.random() * 100) —> [0,99](int)(Math.random() * 100) + 5 ----> [5,104] 3、如何获取[a,b]范围内的随机整数呢?(int)(Math.random() * (b - a + 1)) + a 4、举例 System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}class MathRandomTest {public static void main(String[] args) {double value = Math.random(); System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}  System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}————————————————                      原文链接:https://blog.csdn.net/fj123789/article/details/145672376
  • [技术干货] FastExcel + Java:打造高效灵活的Excel数据导入导出解决方案
    1. 前言在当今的软件开发中,数据的导入与导出是常见的需求,尤其是在企业级应用中,Excel文件作为数据交互的一种重要形式被广泛使用。传统的Excel导入导出功能虽然基本满足需求,但在处理大数据量或需要动态配置时,往往显得效率低下且灵活性不足。本文将围绕在Java中基于实体类的高效Excel数据导入导出展开,介绍如何利用FastExcel这一库实现高性能和灵活的Excel处理。同时,结合动态下拉框和多选下拉框的设置,使得数据的导入导出不仅高效,还能具备较强的可定制性和交互性。通过本篇文章,你将能够掌握在Java中使用实体类驱动Excel导入导出的技术,并学会如何在系统中动态生成下拉框和多选框的配置。2. Fastexcel介绍FastExcel 是一个高性能的 Java 库,旨在提供高效的 Excel 文件操作,尤其是在处理大数据量时,其性能远超常见的POI库。相比其他 Excel 处理库,FastExcel 采用了更加优化的内存管理和流式处理方式,使得它在内存占用和速度上具有显著优势。尤其在导入导出大量数据时,FastExcel 可以有效避免内存溢出和性能瓶颈,保证程序的稳定运行。FastExcel 的特点:高性能: 快速的读取和写入速度,特别适用于大数据量的 Excel 文件。低内存占用: 采用流式读取和写入的方式,极大地减少了内存的使用。简洁易用: 相较于其他复杂的Excel操作库,FastExcel提供了简洁的API接口,易于上手。Excel文件格式支持: 支持 .xlsx 格式的文件,且兼容大部分常见的 Excel 文件操作需求。动态功能扩展: 可以灵活地与 Java 实体类进行绑定,支持动态生成表头、表格内容和格式设置。为什么选择FastExcel?在实际开发中,Excel文件的导入导出经常用于大规模的数据交换,尤其是在财务、报表等领域。对于这些场景,传统的 Excel 处理库(如 Apache POI)可能在面对大数据量时会出现性能瓶颈,尤其是在需要频繁进行读写操作的情况下。而FastExcel通过采用流式读取和写入的方式,有效解决了这一问题,并且通过内存管理优化,使得应用能够在处理大量数据时仍保持高效运行。由于FastExcel的这些优势,它成为了许多Java开发者在实现Excel数据导入导出时的首选工具,尤其是对于需要处理海量数据或需要提高导入导出性能的应用场景。3. 技术实现模拟需求:本案例模拟导出学生的相关信息,包括学号、姓名、性别、父母职业类型和家庭住址所属区域等内容。具体来说,学生信息包含以下字段:学号:唯一标识学生的编号,作为数据的主键。姓名:学生的姓名,文本类型字段。性别:此字段通过下拉框进行选择,支持男、女等选项,方便用户快速选择。父母职业类型:此字段也是一个下拉框,列出了多种常见的职业类型,便于系统自动识别父母的职业分类。家庭住址所属区域:该字段设置为多选下拉框,支持学生家庭地址涉及多个区域的情况。例如,假设某些富裕学生的家庭可能在不同城市或区域拥有多个房产,因此可以选择多个区域。此设置充分考虑了复杂的地址情况,提高了数据录入的灵活性。本文将实现excel文件下拉框基于java程序动态设置,并支持多选下拉框。动态下拉框的实现步骤如下:1. 创建支持宏的xlsm模板文件2. 编写vba代码3. 基于java代码动态写入下拉框数据4. 导出动态设置下拉框的excel模板 3.1. 表结构及实体类说明 1.  PostgreSQL数据库表结构(SQL)我这里创建了一个比较简单的数据字典表存储下拉框数据,完整版数据字典请移步:基于AOP的数据字典实现:实现前端下拉框的可配置更新_数据字典下拉框怎么写-CSDN博客CREATE TABLE students (    student_id VARCHAR(20) PRIMARY KEY,  -- 学号    name VARCHAR(100) NOT NULL,          -- 姓名    gender VARCHAR(10),                  -- 性别    parent_occupation VARCHAR(100),      -- 父母职业类型    home_area TEXT[]                     -- 家庭住址所属区域 (使用数组类型来存储多个区域)); -- 创建一个独立的字典表,用于存储性别、父母职业类型等下拉框选项CREATE TABLE gender_options (    id SERIAL PRIMARY KEY,    gender VARCHAR(10) NOT NULL); CREATE TABLE parent_occupation_options (    id SERIAL PRIMARY KEY,    occupation VARCHAR(100) NOT NULL); -- 插入默认的字典数据INSERT INTO gender_options (gender) VALUES    ('男'),    ('女'); INSERT INTO parent_occupation_options (occupation) VALUES    ('教师'),    ('医生'),    ('工程师'),    ('律师'),    ('其他');在student表中插入10条数据:INSERT INTO students (student_id, name, gender, parent_occupation, home_area)VALUES('S10001', '张三', '男', '教师', '{"北京", "上海"}'),('S10002', '李四', '女', '医生', '{"广州", "深圳"}'),('S10003', '王五', '男', '工程师', '{"北京"}'),('S10004', '赵六', '女', '律师', '{"杭州", "南京"}'),('S10005', '孙七', '男', '商人', '{"上海", "广州"}'),('S10006', '周八', '女', '公务员', '{"北京", "武汉"}'),('S10007', '吴九', '男', '教师', '{"成都"}'),('S10008', '郑十', '女', '护士', '{"重庆", "成都"}'),('S10009', '冯十一', '男', '程序员', '{"深圳", "上海"}'),('S10010', '陈十二', '女', '医生', '{"北京", "上海", "广州"}');2. 实体类@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@ApiModel(value="Students对象", description="")@TableName(value = "students",autoResultMap = true)public class Students implements Serializable {     private static final long serialVersionUID = 1L;     @TableId(value = "student_id", type = IdType.ASSIGN_ID)    @ExcelProperty(value = "学号",index = 0)    private String studentId;     @ExcelProperty(value = "姓名",index = 1)    private String name;     @ExcelProperty(value = "性别",index = 2)    @DropDownSetField(source = {"男", "女"})    private String gender;     @ExcelProperty(value = "父母职业", index = 3)    @DropDownSetField(dynamicSource = ParentOccupationOptions.class)    private String parentOccupation;     @ExcelProperty(value = "所属区域",index = 4,converter = SimpleStringToListConverter.class)    @DropDownSetField(source = {"东城区", "西城区", "海淀区", "朝阳区", "丰台区", "石景山区", "门头沟区", "房山区", "通州区", "顺义区", "昌平区", "大兴区", "怀柔区", "平谷区", "密云区", "延庆区"})    @TableField(typeHandler = StringArrayTypeHandler.class)    private List<String> homeArea; }3.2. 适配PostgresSQL中text[]类型handler编写@ConditionalOnClass({BaseTypeHandler.class})@MappedTypes({List.class})public class StringArrayTypeHandler extends BaseTypeHandler<List<String>> {     @Override    public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType)            throws SQLException {        Connection conn = ps.getConnection();        Array array = conn.createArrayOf("text", parameter.toArray(new String[0]));        ps.setArray(i, array);    }     @Override    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {        Array array = rs.getArray(columnName);        return array == null ? null : Arrays.asList((String[]) array.getArray());    }     @Override    public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {        Array array = rs.getArray(columnIndex);        return array == null ? null : Arrays.asList((String[]) array.getArray());    }     @Override    public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {        Array array = cs.getArray(columnIndex);        return array == null ? null : Arrays.asList((String[]) array.getArray());    }}这个类是一个 MyBatis 的类型处理器(TypeHandler),主要功能是:数据转换:实现 PostgreSQL 数据库中的数组类型与 Java 中的 List<String> 类型之间的双向转换Java -> DB:将 List<String> 转换为 PostgreSQL 的 text[] 数组类型DB -> Java:将 PostgreSQL 的 text[] 数组类型转换为 List<String>应用场景:适用于需要在单个字段中存储多个值的情况,如学生所属区域(可以属于多个区域)的存储和读取技术特点:继承自 BaseTypeHandler<List<String>>使用 @MappedTypes 注解指定处理 List 类型使用 @ConditionalOnClass 实现条件化配置3.3. 注解编写@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface DropDownSetField {    String[] source() default {};    String value() default "";    Class<?>[] dynamicSource() default {};}3.4. 动态设置下拉框核心工具类编写@Component@Slf4jpublic class ExchangeSheetUtils {    @Autowired    private IDropDownDataService dropDownDataService;     private static final int MAX_EXCEL_ROWS = 65536;    private static final String HIDDEN_SHEET_NAME = "字典sheet";    private final char[] alphabet = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};     // 使用ThreadLocal来确保线程安全    private final ThreadLocal<List<String>> dropDownArrays = ThreadLocal.withInitial(ArrayList::new);    private final ThreadLocal<Map<Integer, List<String>>> dropDownMap = ThreadLocal.withInitial(HashMap::new);     // 在方法结束时清理ThreadLocal    public void clearThreadLocals() {        dropDownArrays.get().clear();        dropDownMap.get().clear();    }     /**     * 根据实体类解析字段,并获取动态或固定的下拉数据。     */    public void getEntityField(Class<?> clazz) {        try {            Field[] fields = clazz.getDeclaredFields();            for (Field field : fields) {                    processDropDownField(field);            }        } catch (Exception e) {            log.error("处理实体类字段失败", e);            clearThreadLocals();            throw new RuntimeException("处理实体类字段失败", e);        }    }     /**     * 处理下拉框字段     */    private void processDropDownField(Field field) {        ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);        if (excelProperty == null || excelProperty.value().length == 0) {            log.warn("字段 {} 缺少 ExcelProperty 注解或 value 为空", field.getName());            return;        }                String columnName = excelProperty.value()[0];        dropDownArrays.get().add(columnName);         DropDownSetField dropDownSetField = field.getAnnotation(DropDownSetField.class);        if (dropDownSetField != null) {            List<String> dropDownOptions = new ArrayList<>();             if (dropDownSetField.dynamicSource().length > 0) {                dropDownOptions = getDropDownDataFromDynamicSource(dropDownSetField.dynamicSource());            } else if (dropDownSetField.source().length > 0) {                dropDownOptions = Arrays.asList(dropDownSetField.source());            }             if (!dropDownOptions.isEmpty()) {                int columnIndex = dropDownArrays.get().size() - 1;                dropDownMap.get().put(columnIndex, dropDownOptions);            }        }     }     /**     * 从动态数据源获取下拉数据     */    private List<String> getDropDownDataFromDynamicSource(Class<?>[] dynamicSourceClasses) {        List<String> dropDownOptions = new ArrayList<>();        for (Class<?> dynamicSourceClass : dynamicSourceClasses) {            try {                // 调用动态数据源的接口获取下拉数据(例如通过远程接口)                List<String> data = dropDownDataService.fetchDynamicDropDownData(dynamicSourceClass);                dropDownOptions.addAll(data);            } catch (Exception e) {                log.error("获取动态下拉框数据失败,错误信息: {}", e.getMessage());            }        }        return dropDownOptions;    }          /**     * 创建并更新隐藏Sheet页,添加下拉框     */    public void updateHiddenSheet(Sheet curSheet, Workbook templateWorkbook) {        if (dropDownMap.get().isEmpty()) {            return; // 如果没有下拉框数据则不进行处理        }        DataValidationHelper helper = curSheet.getDataValidationHelper();        String hiddenSheetName = HIDDEN_SHEET_NAME;        Sheet hiddenSheet = templateWorkbook.createSheet(hiddenSheetName);         hideOtherSheets(templateWorkbook);         clearOldNamedRanges(templateWorkbook);         // 填充隐藏Sheet的数据        Set<Map.Entry<Integer, List<String>>> entrySet = dropDownMap.get().entrySet();        for (Map.Entry<Integer, List<String>> entry : entrySet) {            createDropDownList(helper, hiddenSheet, entry);        }    }     /**     * 隐藏所有除第一个外的Sheet     */    private void hideOtherSheets(Workbook templateWorkbook) {        int totalSheets = templateWorkbook.getNumberOfSheets();        for (int i = 1; i < totalSheets; i++) {            templateWorkbook.setSheetHidden(i, true);        }    }     /**     * 清除之前的命名范围     */    private void clearOldNamedRanges(Workbook templateWorkbook) {        for (int i = 0; i < 26; i++) {            Name workbookName = templateWorkbook.getName("dict" + i);            if (workbookName != null) {                templateWorkbook.removeName(workbookName); // 使用 Name 对象删除            }        }    }     /**     * 创建并配置下拉框     */    private void createDropDownList(DataValidationHelper helper, Sheet hiddenSheet, Map.Entry<Integer, List<String>> entry) {        Integer column = entry.getKey();        List<String> values = entry.getValue();         // 填充数据到隐藏sheet        int rowLen = values.size();        for (int i = 0; i < rowLen; i++) {            Row row = hiddenSheet.getRow(i);            if (row == null) {                row = hiddenSheet.createRow(i);            }            Cell cell = row.createCell(column);            cell.setCellValue(values.get(i));        }         String excelColumn = getExcelColumn(column);        String refersTo = HIDDEN_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen;         // 创建命名范围        Name name = hiddenSheet.getWorkbook().createName();        name.setNameName("dict" + column);        name.setRefersToFormula(refersTo);         // 获取第一个sheet(主sheet)        Sheet mainSheet = hiddenSheet.getWorkbook().getSheetAt(0);         // 创建数据验证        DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + column);        CellRangeAddressList addressList = new CellRangeAddressList(1, MAX_EXCEL_ROWS, column, column);        DataValidation validation = helper.createValidation(constraint, addressList);         // 设置验证属性        validation.setSuppressDropDownArrow(true);        validation.setShowErrorBox(true);        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);        validation.createErrorBox("提示", "此值与单元格定义格式不一致!");         // 将验证添加到主sheet        mainSheet.addValidationData(validation);    }    /**     * 将数字列转化为字母列     */    private String getExcelColumn(int num) {        int len = alphabet.length;        int first = num / len;        int second = num % len;         if (num < len) {            return String.valueOf(alphabet[num]);        } else {            return String.valueOf(alphabet[first - 1]) + alphabet[second - 1];        }    }     /**     * 设置数据Sheet页的初始化     */    public void setDataSheet(Sheet sheet, Workbook templateWorkbook) {        Row row = sheet.createRow(0);        List<String> arrays = dropDownArrays.get();        for (int i = 0; i < arrays.size(); i++) {            row.createCell(i).setCellValue(arrays.get(i));        }    } }ExchangeSheetUtils 核心方法说明1. getEntityFieldpublic void getEntityField(Class<?> clazz)功能:解析实体类的字段注解,收集下拉框配置信息处理流程:获取类的所有字段通过 processDropDownField 处理每个字段的注解将下拉框数据存入 ThreadLocal2. processDropDownFieldprivate void processDropDownField(Field field)功能:处理单个字段的下拉框配置处理流程:读取 @ExcelProperty 注解获取列名读取 @DropDownSetField 注解获取下拉选项将数据保存到 dropDownArrays 和 dropDownMap3. updateHiddenSheetpublic void updateHiddenSheet(Sheet curSheet, Workbook templateWorkbook)功能:创建和配置隐藏的数据字典sheet处理流程:创建隐藏sheet清理旧的命名范围通过 createDropDownList 设置下拉框4. createDropDownListprivate void createDropDownList(DataValidationHelper helper, Sheet hiddenSheet, Map.Entry<Integer, List<String>> entry)功能:创建Excel下拉框处理流程:在隐藏sheet中填充下拉选项创建命名范围(Named Range)设置数据验证规则配置下拉框和错误提示这些方法通过 ThreadLocal 实现线程安全,通过 POI 提供的 API 实现 Excel 的各种操作,最终生成一个带有下拉框的 Excel 模板文件。 3.5. 制作多选下拉框exel模板1. 新建.xlsx文件 2. 点击顶部【文件】后点击【选项】 3. 在弹出的弹窗中,点击【信任中心】选项页中的【信任中心设置】按钮 4.  开启宏 5. 另存为.xlsm文件  6. 编写VBA代码选中Sheet,右键弹出菜单,选择【查看代码】,将下面代码粘进去 Sub Worksheet_Change(ByVal Target As Range)    ' 让数据有效性选择可以多选,且不可重复    Dim rngDV As Range    Dim oldVal As String    Dim newVal As String     ' 如果修改的范围超过1个单元格,则退出    If Target.Count > 1 Then GoTo exitHandler     On Error Resume Next    Set rngDV = Cells.SpecialCells(xlCellTypeAllValidation)    On Error GoTo exitHandler     If rngDV Is Nothing Then GoTo exitHandler     If Intersect(Target, rngDV) Is Nothing Then        ' 如果目标单元格不在数据验证区域,什么都不做    Else        Application.EnableEvents = False        newVal = Target.Value         ' 假设字段映射如下:        ' 第3列是 "gender" (性别)        ' 第4列是 "parentOccupation" (父母职业类型)        ' 第5列是 "homeArea" (家庭住址所属区域)        ' 如果修改的是 "gender" 或 "parentOccupation",则是单选,直接替换        ' 如果修改的是 "homeArea",则是多选,去重并追加         If Target.Column = 3 Or Target.Column = 4 Then            ' 对性别(gender)和父母职业类型(parentOccupation)做单选处理            Application.Undo            oldVal = Target.Value            Target.Value = newVal             ' 如果原值与新值不同,直接替换            If oldVal <> newVal Then                Target.Value = newVal            End If        ElseIf Target.Column = 5 Then            ' 对家庭住址所属区域(homeArea)做多选处理            Application.Undo            oldVal = Target.Value            Target.Value = newVal             If oldVal = "" Then                ' 如果原值为空,直接返回            Else                If newVal = "" Then                    ' 如果新值为空,什么都不做                Else                    ' 去除重复项                    If InStr(1, oldVal, newVal) <> 0 Then                        ' 如果新值在旧值中已存在                        If InStr(1, oldVal, newVal) + Len(newVal) - 1 = Len(oldVal) Then                            ' 如果是最后一个选项重复,则删除                            Target.Value = Left(oldVal, Len(oldVal) - Len(newVal) - 1)                        Else                            ' 否则删除逗号后面的重复值                            Target.Value = Replace(oldVal, newVal & ",", "")                        End If                    Else                        ' 如果是新选项,则追加                        Target.Value = oldVal & "," & newVal                    End If                End If            End If        End If    End If exitHandler:    Application.EnableEvents = TrueEnd Sub  这是一个 Excel 工作表的 Worksheet_Change 事件处理程序,主要实现了单元格数据验证的自定义处理逻辑:功能目标:实现单选和多选下拉框的不同处理逻辑具体实现:第3列(性别)和第4列(父母职业)实现单选功能,新值直接替换旧值第5列(所属区域)实现多选功能:允许多个选项,用逗号分隔自动去重(避免重复选择)支持取消选择(点击已选项可移除)使用 Application.Undo 和 Application.EnableEvents 确保操作的原子性和避免事件循环该代码通过 VBA 扩展了 Excel 默认的下拉框功能,使其支持更复杂的业务需求,特别是实现了多选下拉框的去重和动态更新功能。将上述步骤保存,支持动态下拉框的模板文件(.xlsm)就制作完成了。3.6. 导出模板方法     public void exportTemplate(HttpServletResponse response) {        Workbook templateWorkbook = null;        FileInputStream fileInputStream = null;        try {            // 设置响应头            response.setContentType("application/vnd.ms-excel.sheet.macroEnabled.12");            response.setCharacterEncoding("utf-8");            String name = "学生数据模板";            response.setHeader("Content-Disposition", "attachment; filename=" +                    java.net.URLEncoder.encode(name, "UTF-8") + ".xlsm");             // 读取模板文件            File file = new File("D:/学生数据模板.xlsm");            fileInputStream = new FileInputStream(file);            templateWorkbook = WorkbookFactory.create(fileInputStream);             // 获取数据 - 这三个方法的调用顺序不能变            exchangeSheetUtils.getEntityField(Students.class);             Sheet outputSheet = templateWorkbook.getSheetAt(0);            templateWorkbook.setSheetName(0, name);             exchangeSheetUtils.updateHiddenSheet(outputSheet, templateWorkbook);            exchangeSheetUtils.setDataSheet(outputSheet, templateWorkbook);             // 输出文件            templateWorkbook.write(response.getOutputStream());        } catch (Exception e) {            log.error("导出学生模板失败:"+e.getMessage(), e);            throw new RuntimeException("导出模板失败", e);        } finally {            // 清理 ThreadLocal 资源            exchangeSheetUtils.clearThreadLocals();             // 关闭其他资源            if (fileInputStream != null) {                try {                    fileInputStream.close();                } catch (IOException e) {                    log.error("关闭文件流失败", e);                }            }            if (templateWorkbook != null) {                try {                    templateWorkbook.close();                } catch (IOException e) {                    log.error("关闭工作簿失败", e);                }            }        }    }这是一个用于导出 Excel 模板文件的方法,其核心功能是:读取预设的 Excel 模板文件(D:/student.xlsm),通过 ExchangeSheetUtils 工具类解析 Students 实体类的注解信息(@ExcelProperty 和 @DropDownSetField),设置下拉框和数据验证,最后将处理好的模板文件(包含表头、下拉框配置和 VBA 代码)以 .xlsm 格式输出到 HTTP 响应流中。整个过程包含了完整的资源管理(使用 try-finally 确保资源正确关闭)和线程安全处理(通过 clearThreadLocals 清理 ThreadLocal 资源)。关键步骤:设置响应头(.xlsm 格式)读取模板文件处理下拉框配置输出文件清理资源3.7 导入数据方法    @Override    @Transactional(rollbackFor = Exception.class)    public String importData(MultipartFile file) throws IOException {        try {            final List<Students> studentsList = new ArrayList<>();             // 使用Map方式读取数据            EasyExcel.read(file.getInputStream())                    .sheet(0)                    .headRowNumber(1)  // 将表头行设置为1,因为第0行是表头                    .registerReadListener(new AnalysisEventListener<Map<Integer, String>>() {                        @Override                        public void invoke(Map<Integer, String> data, AnalysisContext context) {                            log.info("读取到一行数据: {}", JSON.toJSONString(data));                            // 手动转换为Students对象,使用正确的key                            Students student = new Students();                            student.setStudentId(data.get(0));      // 学号                            student.setName(data.get(1));           // 姓名                            student.setGender(data.get(2));         // 性别                            student.setParentOccupation(data.get(3));  // 家长职业                                                        // 使用与SimpleStringToListConverter相同的逻辑处理homeArea                            String areaStr = data.get(4);                            if (areaStr != null && !areaStr.trim().isEmpty()) {                                student.setHomeArea(Arrays.asList(areaStr.split(",")));                            }                                                        studentsList.add(student);                        }                         @Override                        public void doAfterAllAnalysed(AnalysisContext context) {                            log.info("所有数据解析完成!共读取到 {} 条数据", studentsList.size());                        }                    })                    .doRead();             if (CollectionUtils.isEmpty(studentsList)) {                return "Excel中没有数据";            }             // 保存数据            this.saveBatch(studentsList);            return "导入成功,共导入 " + studentsList.size() + " 条数据";        } catch (Exception e) {            log.error("导入失败:", e);            throw e;        }    }这个 importData 函数的主要功能是导入Excel文件中的学生数据。具体流程如下:使用 @Transactional 注解确保数据导入的事务性,如果出现异常会自动回滚创建一个 studentsList 列表用于存储解析后的数据使用 EasyExcel 读取上传的 Excel 文件:读取第一个 sheet(sheet(0))设置表头行号为1(headRowNumber(1))使用 Map 方式读取数据,其中 key 是列索引(0-4),value 是单元格内容在 invoke 方法中处理每一行数据:将 Map 数据手动转换为 Students 对象特别处理 homeArea 字段,将字符串用逗号分割转换为 List最后批量保存数据到数据库(saveBatch)如果过程中出现异常,会记录错误日志并抛出异常触发事务回滚4. 源码地址源码里面有导出模板,导入数据和导出数据三个接口,实现了功能闭环,完整代码:xfc-fdw-cloud: 公共解决方案5. 结语本文介绍了如何通过 FastExcel 实现高效的 Excel 数据导入导出,基于实体类动态设置excel下拉框(支持多选),解决了实际开发中的常见需求。如有疑问,欢迎在评论区留言,我看到都会回复。————————————————原文链接:https://blog.csdn.net/c18213590220/article/details/145184445
  • [技术干货] Java 后端开发技术学习总结:实用代码示例与项目实操经验大公开
    一、引言在当今数字化时代,Java 后端开发技术占据着举足轻重的地位。从大型企业级应用到小型创业项目,Java 凭借其卓越的性能、强大的生态系统和跨平台特性,成为后端开发的首选语言之一。作为一名在 Java 后端领域摸爬滚打许久的开发者,我深知学习过程中的酸甜苦辣,也积累了不少实用的知识与经验。在这篇博客中,我将毫无保留地与大家分享我的学习总结,涵盖基础语法、常用框架、数据库交互以及项目实战中的宝贵实操经验,同时附上大量实用代码示例,希望能助力各位在 Java 后端开发的道路上少走弯路。二、Java 基础语法夯实数据类型与变量Java 是强类型语言,数据类型分为基本数据类型(如 int、double、char、boolean 等)和引用数据类型(如类、接口、数组)。在定义变量时,务必明确其数据类型,例如:int num = 10;double pi = 3.14;基本数据类型有其默认值,例如 int 默认值为 0,引用数据类型默认值为 null。在变量作用域方面,要注意块级作用域的影响,避免变量重复定义或在不该使用的地方使用。控制结构条件判断语句 if - else 是最常用的结构之一,例如:int score = 85;if (score >= 90) {    System.out.println("优秀");} else if (score >= 80 && score < 90) {    System.out.println("良好");} else {    System.out.println("再接再厉");}循环结构 for、while 和 do - while 各有适用场景。for 循环常用于已知循环次数的情况,如遍历数组:int[] arr = {1, 2, 3, 4, 5};for (int i = 0; i < arr.length; i++) {    System.out.println(arr[i]);}while 循环则适用于条件判断在前,只要条件满足就持续执行的场景,注意要避免死循环,确保循环条件能在某个时刻变为 false。数组与集合数组是固定长度的数据结构,定义时需指定长度:int[] numbers = new int[5];numbers[0] = 1;// 遍历数组for (int num : numbers) {    System.out.println(num);}集合框架(如 ArrayList、LinkedList、HashSet、HashMap 等)提供了更灵活的存储方式。ArrayList 适合频繁的随机访问,示例代码:ArrayList<String> list = new ArrayList<>();list.add("apple");list.add("banana");// 访问元素System.out.println(list.get(0));HashSet 用于去重,HashMap 用于键值对存储,合理选择集合类型能优化代码性能。三、常用 Java 后端框架探秘Spring 框架Spring 是 Java 后端开发的基石,其核心特性 IoC(控制反转)和 AOP(面向切面编程)极大地简化了开发流程。通过依赖注入,对象的创建和管理交给 Spring 容器,例如:@Servicepublic class UserService {    @Autowired    private UserRepository userRepository;    // 业务逻辑}在配置文件(如 application.properties 或 application.yml)中,可以设置数据库连接、端口等信息,Spring Boot 更是凭借其自动配置功能,让项目快速启动。Spring MVC用于构建 Web 应用,它将请求处理、模型、视图进行分离。定义一个简单的 Controller:@RestController@RequestMapping("/api")public class UserController {    @Autowired    private UserService userService;    @GetMapping("/users")    public List<User> getUsers() {        return userService.getAllUsers();    }}这里通过注解指定请求路径和方法类型,方便快捷地处理前端发来的 HTTP 请求,返回相应的数据。MyBatis专注于数据库访问层,它通过 XML 配置文件或注解的方式将 SQL 语句与 Java 方法绑定。在 XML 配置中:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//XML" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.dao.UserDao">    <select id="getUserById" resultType="com.example.entity.User">        SELECT * FROM users WHERE id = #{id}    </select></mapper>对应的 Java 接口:@Mapperpublic interface UserDao {    User getUserById(int id);}这种方式使得 SQL 编写更加灵活,同时与 Java 代码解耦,方便维护。四、数据库交互实战JDBC(Java Database Connectivity)基础JDBC 是 Java 连接数据库的底层 API,首先需要加载驱动:Class.forName("com.mysql.cj.jdbc.Driver");然后建立连接:Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");执行 SQL 语句,例如查询:Statement statement = connection.createStatement();ResultSet resultSet = statement.executeQuery("SELECT * FROM users");while (resultSet.next()) {    System.out.println(resultSet.getString("name"));}最后记得关闭连接、语句和结果集,释放资源。使用连接池优化数据库连接频繁创建和关闭数据库连接开销较大,连接池(如 HikariCP、Druid 等)能有效解决这个问题。以 HikariCP 为例,配置连接池:HikariConfig hikariConfig = new HikariConfig();hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");hikariConfig.setUsername("root");hikariConfig.setPassword("password");HikariDataSource dataSource = new HikariDataSource(hikariConfig);从连接池中获取连接:Connection connection = dataSource.getConnection();连接池会自动管理连接的复用、空闲连接回收等,提升性能。数据库事务处理在涉及多个数据库操作且需保证原子性时,事务至关重要。使用 Spring 的事务管理,在 Service 层方法上添加注解:@Transactionalpublic void transferMoney(int fromUserId, int toUserId, double amount) {    // 从一个用户账户扣除金额    userRepository.decreaseBalance(fromUserId, amount);    // 向另一个用户账户增加金额    userRepository.increaseBalance(toUserId, amount);}若其中一个操作失败,整个事务将回滚,确保数据的一致性。五、项目实操经验大放送项目架构设计在启动一个项目前,合理的架构设计能避免后期的混乱。通常采用分层架构,如表现层(Controller)、业务逻辑层(Service)、数据访问层(Dao)、实体层(Entity)和工具层(Utils)。各层职责明确,相互协作,降低耦合度。考虑扩展性,例如设计接口时预留扩展字段,以便后续功能迭代时能轻松兼容新需求。日志管理引入日志框架(如 Log4j、Logback 等),在关键代码段添加日志记录:private static final Logger logger = LoggerFactory.getLogger(MyClass.class);public void doSomething() {    logger.info("开始执行 doSomething 方法");    // 业务逻辑    logger.info("doSomething 方法执行完毕");}通过配置日志级别,可以灵活控制日志输出的详细程度,方便在开发、测试和生产环境中排查问题。异常处理统一的异常处理机制能提升系统的稳定性。在 Spring 项目中,可以定义全局异常处理器:@ControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(Exception.class)    public ResponseEntity<String> handleException(Exception e) {        logger.error("系统出现异常:", e);        return new ResponseEntity<>("系统繁忙,请稍后再试", HttpStatus.INTERNAL_SERVER_ERROR);    }}针对不同类型的异常(如业务异常、数据库异常等),可以提供个性化的错误提示,提升用户体验。六、总结与展望通过对 Java 后端开发技术从基础语法到框架应用,再到数据库交互和项目实战的深入学习,我们逐步构建起坚实的知识体系。然而,技术的发展日新月异,Java 生态也在不断演进。未来,我们需要持续关注新的框架版本、性能优化技巧以及云计算、微服务等新兴领域与 Java 后端的融合。希望这篇学习总结能成为各位读者在 Java 后端之旅中的得力伙伴,让我们一起在代码的世界里不断探索,创造出更强大、更高效的后端应用。————————————————原文链接:https://blog.csdn.net/2301_80350265/article/details/145096011
  • [技术干货] Java 中的泛型(超全详解)
    一、泛型概述1. 什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错 那参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。 泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 //没有泛型的时候,集合如何存储数据//结论://如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型//此时可以往集合添加任意的数据类型。//带来一个坏处:我们在获取数据的时候,无法使用他的特有行为。 //此时推出了泛型,可以在添加数据的时候就把类型进行统一。//而且我们在获取数据的时候,也省的强转了,非常的方便。2. 泛型使用场景在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。 public class demo1 {    public static void main(String[] args) {        ArrayList<String> list=new ArrayList<>();        list.add("a");        list.add("b");        list.add("c");        for(String s:list){            System.out.println(s);        }    }}上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:  public static void main(String[] args) {        ArrayList list = new ArrayList();        list.add("aaa");        list.add("bbb");        list.add("ccc");        list.add(666);         for (int i = 0; i < list.size(); i++) {            System.out.println((String)list.get(i));        }    }  上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。 使用泛型代码如下:  public static void main(String[] args) {        ArrayList<String> list = new ArrayList();        list.add("aaa");        list.add("bbb");        list.add("ccc");        //list.add(666);// 在编译阶段,编译器会报错                for (int i = 0; i < list.size(); i++) {            System.out.println(list.get(i));        }    }< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。3.总结泛型的出现就是为了统一集合当中数据类型的   二、泛型类泛型类的定义  尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:   T :代表一般的任何类。  E :代表 Element 元素的意思,或者 Exception 异常的意思。  K :代表 Key 的意思。  V :代表 Value 的意思,通常与 K 一起配合使用。  S :代表 Subtype 的意思,文章后面部分会讲解示意。 自己实现集合 代码如下: package fangxing; import java.util.Arrays; public class MyArrayList<E> {    Object[] obj = new Object[10];    int size = 0;     /*    E: 表示不确定的类型,该类型在类名后面已经定义过了    e: 形参的名字,变量名     */    public boolean add(E e) {        obj[size++] = e;        return true;        //当添加成功以后,集合还是会把这些数据当做Object类型处理    }     public E get(int index) {        return (E) obj[index];        //获取的时候集合在把他强转<E>类型     }     @Override    public String toString() {           return Arrays.toString(obj);     }}package fangxing; import javax.xml.stream.events.StartDocument; public class demo3 {    public static void main(String[] args) {        MyArrayList<String> list = new MyArrayList<>();        list.add("aaa");        list.add("bbb");        list.add("ccc");        System.out.println(list);    }}三、泛型方法    格式     package fangxing; import java.util.ArrayList;import java.util.Arrays;import java.util.List; public class ListUtil {    private ListUtil() {    }     /*    参数一:集合    参数二: 最后要添加的元素     */    public static <E> void addAll(ArrayList<E> list, E e1, E e2) {        list.add(e1);        list.add(e2);    } } package fangxing; import java.util.ArrayList; public class demo4 {    public static void main(String[] args) {        ArrayList<String>list=new ArrayList<>();        ListUtil.addAll(list,"zhangsan","lisi");        System.out.println(list);//[zhangsan, lisi]     }}  添加很多元素 public static <E> void addAll(ArrayList<E> list, E ...e1) {        for (E e : e1) {          list.add(e);        }四、泛型接口  方法1:实现类给出具体类型 举例: public class MyArrayList2  implements List<String> public static void main(String[] args) {        MyArrayList2 list2=new MyArrayList2();    }方法2: 实现类延续泛型,创建对象再确定 public class MyArrayList3 <E>  implements List<E>  MyArrayList3<String> list = new MyArrayList3<>();五、类型擦除1. 什么是类型擦除 泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。 换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。 其实Java中的泛型本质是伪泛型 当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型,  如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型  当代码编译到class文件的时候,泛型就消失,叫泛型的擦除 看一个例子,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。 public class GenericType {    public static void main(String[] args) {          ArrayList<String> arrayString = new ArrayList<String>();           ArrayList<Integer> arrayInteger = new ArrayList<Integer>();           System.out.println(arrayString.getClass() == arrayInteger.getClass());// true    }  }在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。 明明我们在 <> 中传入了两种不同的数据类型,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。 那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢? 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释 2. 类型擦除的原理假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问: 不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢? 换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢? Java 是如何解决这个问题的? 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。 可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。 当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。  举例如下: public class GenericType {    public static void main(String[] args) {          ArrayList<Integer> arrayInteger = new ArrayList<Integer>();// 设置验票系统           arrayInteger.add(111);// 观众进场,验票系统验票,门票会被收走(类型擦除)        Integer n = arrayInteger.get(0);// 获取观众信息,编译器会进行强制类型转换        System.out.println(n);    }  }擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令: 对原始方法 get() 的调用,返回的是 Object 类型;将返回的 Object 类型强制转换为 Integer 类型; 代码如下: Integer n = arrayInteger.get(0);// 这条代码底层如下: //(1)get() 方法的返回值返回的是 Object 类型Object object = arrayInteger.get(0);//(2)编译器自动插入 Integer 的强制类型转换Integer n = (Integer) object;3. 类型擦除小结 1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析); 2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。 六、泛型通配符1. 泛型的继承泛型不具备继承性,但是数据具备继承性  此时,泛型里面写的什么类型,那么就传递什么类型的数据 泛型不具备继承性举例 package fangxing; import java.util.ArrayList; public class demo5 {    public static void main(String[] args) {        /*        泛型不具备继承性,但是数据具备继承性         */        ArrayList<Ye> list1=new ArrayList<>();        ArrayList<Fu> list2=new ArrayList<>();        ArrayList<Zi> list3=new ArrayList<>();        //调用method方法        method(list1);        //method(list2);//编译错误//method(list3);//编译错误    }    /*    此时,泛型里面写的什么类型,那么就传递什么类型的数据     */    public static  void method(ArrayList<Ye> list){    }}class Ye{}class Fu extends Ye{}class Zi extends Fu{}数据具备继承性     //数据具备继承性        list1.add(new Ye());//添加爷爷的对象等        list1.add(new Fu());        list1.add(new Zi());定义一个方法,形参是一个集合,但是集合中的数据类型不确定。应用场景:*      1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。*      2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符* 泛型的通配符:*      关键点:可以限定类型的范围。/* * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。 * 弊端: *      利用泛型方法有一个小弊端,此时他可以接受任意的数据类型 *      Ye  Fu   Zi    Student * * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi * * 此时我们就可以使用泛型的通配符: *      ?也表示不确定的类型 *      他可以进行类型的限定 *      ? extends E: 表示可以传递E或者E所有的子类类型 *      ? super E:表示可以传递E或者E所有的父类类型 *举例 package fangxing; import java.util.ArrayList; /* *   需求: *       定义一个方法,形参是一个集合,但是集合中的数据类型不确定。 * * *//* * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。 * 弊端: *      利用泛型方法有一个小弊端,此时他可以接受任意的数据类型 *      Ye  Fu   Zi    Student * * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi * * 此时我们就可以使用泛型的通配符: *      ?也表示不确定的类型 *      他可以进行类型的限定 *      ? extends E: 表示可以传递E或者E所有的子类类型 *      ? super E:表示可以传递E或者E所有的父类类型 * * 应用场景: *      1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。 *      2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符 * 泛型的通配符: *      关键点:可以限定类型的范围。 * * */public class demo6 {    public static void main(String[] args) {        //创建集合的对象        ArrayList<Ye> list1 = new ArrayList<>();        ArrayList<Fu> list2 = new ArrayList<>();        ArrayList<Zi> list3 = new ArrayList<>();        ArrayList<Student2> list4 = new ArrayList<>();        method(list1);        method(list2);        //method(list3);        //method(list4);    }    public static void method(ArrayList<? super Fu> list) {    }}class Ye {}class Fu extends Ye {}class Zi extends Fu {}class Student2{}2.练习/*      需求:          定义一个继承结构:                              动物                   |                           |                   猫                          狗                |      |                    |      |             波斯猫   狸花猫                泰迪   哈士奇           属性:名字,年龄           行为:吃东西                 波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干                 狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼                 泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭                 哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家       测试类中定义一个方法用于饲养动物          public static void keepPet(ArrayList<???> list){              //遍历集合,调用动物的eat方法          }      要求1:该方法能养所有品种的猫,但是不能养狗      要求2:该方法能养所有品种的狗,但是不能养猫      要求3:该方法能养所有的动物,但是不能传递其他类型   */测试类 package lx; import java.util.ArrayList; public class demo1 {    /*          需求:              定义一个继承结构:                                  动物                       |                           |                       猫                          狗                    |      |                    |      |                 波斯猫   狸花猫                泰迪   哈士奇               属性:名字,年龄               行为:吃东西                     波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干                     狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼                     泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭                     哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家          测试类中定义一个方法用于饲养动物              public static void keepPet(ArrayList<???> list){                  //遍历集合,调用动物的eat方法              }          要求1:该方法能养所有品种的猫,但是不能养狗          要求2:该方法能养所有品种的狗,但是不能养猫          要求3:该方法能养所有的动物,但是不能传递其他类型       */    public static void main(String[] args) {        HuskyDog h = new HuskyDog("哈士奇", 1);        LihuaCat l = new LihuaCat("狸花猫", 2);        PersianCat p = new PersianCat("波斯猫", 3);        TeddyDog t = new TeddyDog("泰迪", 4);        ArrayList<LihuaCat> list1 = new ArrayList<>();        ArrayList<PersianCat> list2 = new ArrayList<>();        // 向列表中添加一些猫的实例        list1.add(l);        list2.add(p);        //调用方法        keepPet1(list1);        keepPet1(list2);        System.out.println("-------------------------------------------");        ArrayList<HuskyDog> list3 = new ArrayList<>();        ArrayList<TeddyDog> list4 = new ArrayList<>();        // 向列表中添加一些狗的实例        list3.add(h);        list4.add(t);        //调用方法        keepPet2(list3);        keepPet2(list4);        System.out.println("-------------------------------------------");        list1.add(l);        list2.add(p);        list3.add(h);        list4.add(t);        keepPet3(list1);        keepPet3(list2);        keepPet3(list3);        keepPet3(list4);    }      /*    此时我们就可以使用泛型的通配符:      ?也表示不确定的类型      他可以进行类型的限定      ? extends E: 表示可以传递E或者E所有的子类类型     ? super E:表示可以传递E或者E所有的父类类型     */    //  要求1:该方法能养所有品种的猫,但是不能养狗    public static void keepPet1(ArrayList<? extends Cat> list) {        //遍历集合,调用动物的eat方法        for (Cat cat : list) {            cat.eat();        }    }    //  要求2:该方法能养所有品种的狗,但是不能养猫    public static void keepPet2(ArrayList<? extends Dog> list) {        //遍历集合,调用动物的eat方法        for (Dog dog : list) {            dog.eat();        }    }    //  要求3:该方法能养所有的动物,但是不能传递其他类型    public static void keepPet3(ArrayList<? extends Animal> list) {        //遍历集合,调用动物的eat方法        for (Animal animal : list) {            animal.eat();        }    }}Animal类 package lx; public abstract class Animal {    private String name;    private int age;     public Animal() {    }     public Animal(String name, int age) {        this.name = name;        this.age = age;    }     /**     * 获取     * @return name     */    public String getName() {        return name;    }     /**     * 设置     * @param name     */    public void setName(String name) {        this.name = name;    }     /**     * 获取     * @return age     */    public int getAge() {        return age;    }     /**     * 设置     * @param age     */    public void setAge(int age) {        this.age = age;    }     public String toString() {        return "Animal{name = " + name + ", age = " + age + "}";    }    public abstract  void eat();}cat类型 package lx; public abstract class Cat extends Animal{     public Cat() {    }     public Cat(String name, int age) {        super(name, age);    }}Dog类 package lx; public abstract class Dog extends Animal{    public Dog() {    }     public Dog(String name, int age) {        super(name, age);    }}哈士奇类 package lx; public class HuskyDog extends Dog{    @Override    public void eat() {        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的哈士奇,正在吃骨头,边吃边拆家");     }     public HuskyDog() {    }     public HuskyDog(String name, int age) {        super(name, age);    }} 狸花猫类 package lx; public class LihuaCat extends Cat {    @Override    public void eat() {        System.out.println("一只叫做" + getName() + "的," + getAge() + "岁的狸花猫,正在吃鱼");    }     public LihuaCat() {    }     public LihuaCat(String name, int age) {        super(name, age);    }}波斯猫类 package lx; public class PersianCat extends Cat{    @Override    public void eat() {        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的波斯猫,正在吃小饼干");    }     public PersianCat() {    }     public PersianCat(String name, int age) {        super(name, age);    }}泰迪猫类 package lx; public class TeddyDog extends Dog{    @Override    public void eat() {        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁泰迪,正在吃骨头,边吃边蹭");    }     public TeddyDog() {    }     public TeddyDog(String name, int age) {        super(name, age);    }}————————————————           原文链接:https://blog.csdn.net/weixin_65752158/article/details/140321808
  • [技术干货] 入门篇:从0到1搭建 Java、Spring Boot、Spring Security 企业级权限管理系统
    前言本文基于 Java 和 Spring Boot 3,从 0 到 1 完成一个企业级后端项目的开发。依次整合 MySQL 和 Redis,实现基础的增删改查(CRUD)接口,并通过 Spring Security 完成登录认证与接口权限控制,最终构建完整的企业级安全管理框架。 作为开源项目youlai-boot 的入门篇,本文旨在帮助前端开发者或后端初学者快速上手 Java 后端开发。通过一步步实践,掌握项目的核心逻辑与实现细节,不仅能放心使用,还能轻松扩展和二次开发 环境准备本章节介绍安装 Java 开发所需的环境,包括 JDK、Maven 和 IntelliJ IDEA(简称 IDEA),这些工具是 Java 开发的核心环境。 安装 JDKJDK(Java Development Kit) 是 Java 开发工具包,包含编译器、运行时环境等,支持 Java 应用程序的开发与运行。 下载 JDK访问以下链接,下载最新版本的 JDK 安装包:https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe 安装 JDK下载完成后,双击安装包,根据引导完成安装。示例安装路径:D:\Java\jdk-17.0.3.1 配置环境变量打开 系统属性 -> 高级系统设置 -> 环境变量新建系统变量 JAVA_HOME,值为 D:\Java\jdk-17.0.3.1  在 Path 环境变量中,添加 %JAVA_HOME%\bin  验证安装在命令行中执行以下命令,查看 Java 版本: java -version 输出类似如下内容表示安装成功:   安装 MavenMaven 是一个流行的 Java 构建和依赖管理工具,类似于前端的 npm,用于管理项目的构建流程及第三方依赖库。 下载 Maven访问以下链接,下载最新的 bin.zip 文件:https://maven.apache.org/download.cgi 将 bin.zip 解压到本地目录,示例解压路径:D:\Soft\apache-maven-3.9.5 配置阿里云镜像编辑配置文件 D:\Soft\apache-maven-3.9.5\conf\settings.xml,在 <mirrors> 节点中添加以下配置: <mirrors>    <mirror>        <id>alimaven</id>        <name>aliyun maven</name>        <url>http://maven.aliyun.com/nexus/content/groups/public/</url>        <mirrorOf>central</mirrorOf>           </mirror></mirrors> 配置环境变量新建系统变量 M2_HOME,值为 D:\Soft\apache-maven-3.9.5  在 Path 环境变量中,添加 %M2_HOME%\bin  验证安装在命令行中执行以下命令,查看 Maven 版本: mvn -v1输出类似如下内容表示安装成功:   安装 IDEAIntelliJ IDEA 是一款功能强大的 Java 集成开发环境(IDE),由 JetBrains 开发,广泛用于 Java 项目的开发、调试和运行。 下载 IDEA访问以下链接,下载适合您系统的安装包:https://www.jetbrains.com/idea/download 安装 IDEA下载完成后,双击安装包,按引导完成安装即可。   具体的配置在创建项目之前说明。 安装 MySQL安装 MySQL 服务Windows 安装 MySQL 8:https://youlai.blog.csdn.net/article/details/133272887Linux 安装 MySQL 8:https://youlai.blog.csdn.net/article/details/130398179安装 MySQL 可视化工具推荐使用 Navicat,这是一款功能强大的数据库管理工具,但需要付费。如果你因未付费而遇到使用限制,可以选择 DBeaver 作为替代方案。 Navicat 官方下载地址:https://www.navicat.com.cn/download/navicat-premiumDBeaver 官方下载地址:https://dbeaver.io/下载并安装 Navicat 后,你将获得 14 天的免费试用期。安装完成后,连接到 MySQL 服务,即可对数据库和表进行可视化操作,体验非常流畅。 Navicat 界面效果:   安装 Redis安装 Redis 服务Windows 安装 Redis:https://youlai.blog.csdn.net/article/details/133410293Linux 安装 Redis:https://youlai.blog.csdn.net/article/details/130439335安装 Redis 可视化工具推荐使用开源的 AnotherRedisDesktopManager,这是一款功能强大且免费的 Redis 可视化工具。 AnotherRedisDesktopManager 下载地址:https://gitee.com/qishibo/AnotherRedisDesktopManager/releases  安装步骤: 下载安装程序(.exe 文件)。按照安装向导逐步操作,完成安装。使用步骤: 打开软件,点击“新建连接”。输入 Redis 服务器的连接信息(如主机地址、端口、密码等)。连接成功后,即可对 Redis 数据进行可视化操作。AnotherRedisDesktopManager 界面效果: 连接成功示例:  项目搭建新建项目打开 IDEA,选择 Projects → New Project。 项目名称:输入 youlai-boot(可根据实际需求调整)。项目类型:选择 Maven。JDK 版本:选择 JDK 17(确保已安装 JDK 17)。项目信息:Group:填写 com.youlai(可根据实际需求调整)。Artifact:填写 youlai-boot(可根据实际需求调整)。Package name:填写 com.youlai.boot(可根据实际需求调整)。 点击 Next,在左侧的依赖列表中勾选项目所需的依赖。 完成项目初始化后,项目结构如下 配置开发环境配置 JDK通过 File → Project Structure(快捷键 Ctrl + Alt + Shift + S)打开项目结构配置面板,确保 Project 和 Modules 使用的 SDK 版本为前面安装的 JDK 17。 配置 Maven通过 File → Settings(快捷键 Ctrl + Alt + S)打开设置面板,切换到 Maven 选项,并将 Maven 设置为前面安装到本地的版本。 验证配置在 IDEA 的 Terminal 中输入以下命令 mvn -v,验证 Maven 是否正确使用了 JDK 17: 快速开始创建第一个接口在 src/main/java 目录下的 com.youlai.boot 包中,新建一个名为 controller 的包。在 controller 包下创建一个名为 TestController 的 Java 类。在 TestController 类中添加一个简单的 hello-world 接口。  以下是 TestController 类的代码: /** * 测试接口 * * @author youlai */@RestControllerpublic class TestController {     @GetMapping("/hello-world")    public String test() {        return "hello world";    } } 启动测试在项目的右上角,点击 🐞 (手动绿色)图标以调试运行项目。   控制台显示 Tomcat 已在端口 8080 (http) ,表示应用成功启动   打开浏览器,访问 http://localhost:8080/hello-world,页面将显示 hello world,表示接口正常运行。   连接数据库为实现应用与 MySQL 的连接与操作,整合 MyBatis-Plus 可简化数据库操作,减少重复的 CRUD 代码,提升开发效率,实现高效、简洁、可维护的持久层开发。 创建数据库使用 MySQL 可视化工具 (Navicat) 执行下面脚本完成数据库的创建名为 youlai-boot 的数据库,其中包含测试的用户表     -- ----------------------------    -- 1. 创建数据库    -- ----------------------------    CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;     -- ----------------------------    -- 2. 创建表 && 数据初始化    -- ----------------------------    use youlai_boot; -- ----------------------------    -- Table structure for sys_user    -- ----------------------------    DROP TABLE IF EXISTS `sys_user`;    CREATE TABLE `sys_user` (        `id` int NOT NULL AUTO_INCREMENT,        `username` varchar(64) NULL DEFAULT NULL COMMENT '用户名',        `nickname` varchar(64) NULL DEFAULT NULL COMMENT '昵称',        `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别(1-男 2-女 0-保密)',        `password` varchar(100) NULL DEFAULT NULL COMMENT '密码',        `dept_id` int NULL DEFAULT NULL COMMENT '部门ID',        `avatar` varchar(255) NULL DEFAULT '' COMMENT '用户头像',        `mobile` varchar(20) NULL DEFAULT NULL COMMENT '联系方式',        `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',        `email` varchar(128) NULL DEFAULT NULL COMMENT '用户邮箱',        `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',        `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',        `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',        `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',        `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',        PRIMARY KEY (`id`) USING BTREE    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;     -- ----------------------------    -- Records of sys_user    -- ----------------------------    INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0);    INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', now(), NULL, now(), NULL, 0);    INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);  添加依赖项目 pom.xml 添加 MySQL 驱动和 Mybatis-Plus 依赖: <!-- MySQL 8 驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>9.1.0</version><scope>runtime</scope></dependency> <!-- Druid 数据库连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.24</version></dependency> <!-- MyBatis Plus Starter--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version></dependency> 配置数据源将 src/main/resources/application.properties 文件修改为 src/main/resources/application.yml,因为我们更倾向于使用 yml 格式。然后,在 yml 文件中添加以下内容: server:  port: 8080  spring:  datasource:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/youlai_boot?useSSL=false&serverTimezone=Asia/Shanghai&&characterEncoding=utf8    username: root    password: 123456 mybatis-plus:  configuration:    # 驼峰命名映射    map-underscore-to-camel-case: true    # 打印 sql 日志    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  global-config:    db-config:      id-type: auto # 主键策略      logic-delete-field: is_deleted # 全局逻辑删除字段(可选) 增删改查接口安装 MybatisX 插件在 IDEA 中依次点击 File → Settings(快捷键 Ctrl + Alt + S),打开设置面板,切换到 Plugins 选项卡,搜索 MybatisX 并安装插件。   自动代码生成在 IDEA 右侧导航栏点击 Database,打开数据库配置面板,选择新增数据源。   输入数据库的 主机地址、用户名 和 密码,测试连接成功后点击 OK 保存。  配置完数据源后,展开数据库中的表,右击 sys_user 表,选择 MybatisX-Generator 打开代码生成面板。   设置代码生成的目标路径,并选择 Mybatis-Plus 3 + Lombok 代码风格。  点击 Finish 生成,自动生成相关代码。 MybatisX 生成的代码存在以下问题: SysUserMapper.java 文件未标注 @Mapper 注解,导致无法被 Spring Boot 识别为 Mybatis 的 Mapper 接口。如果已配置 @MapperScan,可以省略此注解,但最简单的方法是直接在 SysUserMapper.java 文件中添加 @Mapper 注解。注意避免导入错误的包。  添加增删改查接口在 controller 包下创建 UserController.java,编写用户管理接口: /** * 用户控制层 * * @author youlai * @since 2024/12/04 */@RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController {     private final SysUserService userService;     /**     * 获取用户列表     */    @GetMapping    public List<SysUser> listUsers() {        return userService.list();    }     /**     * 获取用户详情     */    @GetMapping("/{id}")    public SysUser getUserById(@PathVariable Long id) {        return userService.getById(id);    }     /**     * 新增用户     */    @PostMapping    public String createUser(@RequestBody SysUser user) {        userService.save(user);        return "用户创建成功";    }     /**     * 更新用户信息     */    @PutMapping("/{id}")    public String updateUser(@PathVariable Long id, @RequestBody SysUser user) {        userService.updateById(user);        return "用户更新成功";    }     /**     * 删除用户     */    @DeleteMapping("/{id}")    public String deleteUser(@PathVariable Long id) {        userService.removeById(id);        return "用户删除成功";    } } 接口测试重新启动应用,在浏览器中访问 http://localhost:8080/users,查看用户数据。   其他增删改接口可以通过后续整合接口文档进行测试。 集成 Knife4j 接口文档Knife4j 是基于 Swagger2 和 OpenAPI3 的增强解决方案,旨在提供更友好的界面和更多功能扩展,帮助开发者更便捷地调试和测试 API。以下是通过参考 Knife4j 官方文档 Spring Boot 3 整合 Knife4j 实现集成的过程。 添加依赖在 pom.xml 文件中引入 Knife4j 的依赖: <dependency>    <groupId>com.github.xiaoymin</groupId>    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>    <version>4.5.0</version></dependency> 配置接口文档在 application.yml 文件中进行配置。注意,packages-to-scan 需要配置为项目的包路径,以确保接口能够被正确扫描,其他配置保持默认即可。 # springdoc-openapi 项目配置springdoc:  swagger-ui:    path: /swagger-ui.html    tags-sorter: alpha    operations-sorter: alpha  api-docs:    path: /v3/api-docs  group-configs:    - group: 'default'      paths-to-match: '/**'      packages-to-scan: com.youlai.boot.controller # 需要修改成自己项目的接口包路径# knife4j的增强配置,不需要增强可以不配knife4j:  enable: true  # 是否为生产环境,true 表示生产环境,接口文档将被禁用  production: false  setting:    language: zh_cn # 设置文档语言为中文 添加接口文档配置,在 com.youlai.boot.config 添加 OpenApiConfig 接口文档配置 package com.youlai.boot.config; import io.swagger.v3.oas.models.Components;import io.swagger.v3.oas.models.OpenAPI;import io.swagger.v3.oas.models.info.Info;import io.swagger.v3.oas.models.security.SecurityRequirement;import io.swagger.v3.oas.models.security.SecurityScheme;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springdoc.core.customizers.GlobalOpenApiCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.http.HttpHeaders; /** * OpenAPI 接口文档配置 * * @author youlai */@Configuration@RequiredArgsConstructor@Slf4jpublic class OpenApiConfig {     private final Environment environment;     /**     * 接口信息     */     @Bean    public OpenAPI openApi() {         String appVersion = environment.getProperty("project.version", "1.0.0");         return new OpenAPI()                .info(new Info()                        .title("系统接口文档")                        .version(appVersion)                )                // 配置全局鉴权参数-Authorize                .components(new Components()                        .addSecuritySchemes(HttpHeaders.AUTHORIZATION,                                new SecurityScheme()                                        .name(HttpHeaders.AUTHORIZATION)                                        .type(SecurityScheme.Type.APIKEY)                                        .in(SecurityScheme.In.HEADER)                                        .scheme("Bearer")                                        .bearerFormat("JWT")                        )                );    }      /**     * 全局自定义扩展     * <p>     * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。     * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。     */    @Bean    public GlobalOpenApiCustomizer globalOpenApiCustomizer() {        return openApi -> {            // 全局添加鉴权参数            if (openApi.getPaths() != null) {                openApi.getPaths().forEach((s, pathItem) -> {                    // 登录接口/验证码不需要添加鉴权参数                    if ("/api/v1/auth/login".equals(s)) {                        return;                    }                    // 接口添加鉴权参数                    pathItem.readOperations()                            .forEach(operation ->                                    operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))                            );                });            }        };    } } 完善接口文档完善接口描述 在已有的 REST 接口中,使用 OpenAPI 规范注解来描述接口的详细信息,以便通过 Knife4j 生成更加清晰的接口文档。以下是如何为用户的增删改查接口添加文档描述注解的示例: @Tag(name = "用户接口")@RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController {     private final SysUserService userService;     @Operation(summary = "获取用户列表")    @GetMapping    public List<SysUser> listUsers() {        return userService.list();    }     @Operation(summary = "获取用户详情")    @GetMapping("/{id}")    public SysUser getUserById(            @Parameter(description = "用户ID") @PathVariable Long id    ) {        return userService.getById(id);    }     @Operation(summary = "新增用户")    @PostMapping    public String createUser(@RequestBody SysUser user) {        userService.save(user);        return "新增用户成功";    }     @Operation(summary = "修改用户")    @PutMapping("/{id}")    public String updateUser(            @Parameter(description = "用户ID") @PathVariable Long id,            @RequestBody SysUser user    ) {        userService.updateById(user);        return "修改用户成功";    }     @Operation(summary = "删除用户")    @DeleteMapping("/{id}")    public String deleteUser(            @Parameter(description = "用户ID") @PathVariable Long id    ) {        userService.removeById(id);        return "用户删除成功";    } } 完善实体类描述 在 SysUser 实体类中为每个字段添加 @Schema 注解,用于在接口文档中显示字段的详细说明及示例值: @Schema(description = "用户对象")@TableName(value = "sys_user")@Datapublic class SysUser implements Serializable {     @Schema(description = "用户ID", example = "1")    @TableId(type = IdType.AUTO)    private Integer id;     @Schema(description = "用户名", example = "admin")    private String username;     @Schema(description = "昵称", example = "管理员")    private String nickname;     @Schema(description = "性别(1-男,2-女,0-保密)", example = "1")    private Integer gender;     @Schema(description = "用户头像URL", example = "https://example.com/avatar.png")    private String avatar;     @Schema(description = "联系方式", example = "13800000000")    private String mobile;     @Schema(description = "用户邮箱", example = "admin@example.com")    private String email;        // ... } 使用接口文档完成以上步骤后,重新启动应用并访问生成的接口文档。 Swagger UI 文档地址:http://localhost:8080/swagger-ui/index.htmlKnife4j 文档地址:http://localhost:8080/doc.html通过左侧的接口列表查看增删改查接口,并点击具体接口查看详细参数说明及示例值:   接着,可以通过接口文档新增用户,接口返回成功后,可以看到数据库表中新增了一条用户数据:   集成 Redis 缓存Redis 是当前广泛使用的高性能缓存中间件,能够显著提升系统性能,减轻数据库压力,几乎成为现代应用的标配。 添加依赖在 pom.xml 文件中添加 Spring Boot Redis 依赖: <dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 配置 Redis 连接在 application.yml 文件中配置 Redis 连接信息: spring:  data:    redis:      database: 0    # Redis 数据库索引      host: localhost  # Redis 主机地址      port: 6379  # Redis 端口      # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串      password: 123456      timeout: 10s 自定义序列化Spring Boot 默认使用 JdkSerializationRedisSerializer 进行序列化。我们可以通过自定义 RedisTemplate,将其修改为更易读的 String 和 JSON 序列化方式: /** *  Redis 自动装配配置 * * @author youlai * @since 2024/12/5 */@Configurationpublic class RedisConfig {     /**     * 自定义 RedisTemplate     * <p>     * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer     *     * @param redisConnectionFactory {@link RedisConnectionFactory}     * @return {@link RedisTemplate}     */    @Bean    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();        redisTemplate.setConnectionFactory(redisConnectionFactory);         redisTemplate.setKeySerializer(RedisSerializer.string());        redisTemplate.setValueSerializer(RedisSerializer.json());         redisTemplate.setHashKeySerializer(RedisSerializer.string());        redisTemplate.setHashValueSerializer(RedisSerializer.json());         redisTemplate.afterPropertiesSet();        return redisTemplate;    } } 单元测试在 src/test/java 目录的 com.youlai.boot 包下创建 RedisTests 单元测试类,用于验证数据的存储与读取。 @SpringBootTest@Slf4jclass RedisTests {     @Autowired    private RedisTemplate<String, Object> redisTemplate;     @Autowired    private SysUserService userService;     @Test    void testSetAndGet() {        Long userId = 1L;        // 1. 从数据库中获取用户信息        SysUser user = userService.getById(userId);        log.info("从数据库中获取用户信息: {}", user);         // 2. 将用户信息缓存到 Redis        redisTemplate.opsForValue().set("user:" + userId, user);         // 3. 从 Redis 中获取缓存的用户信息        SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get("user:" + userId);        log.info("从 Redis 中获取用户信息: {}", cachedUser);    }} 点击测试类方法左侧的图标运行单元测试。   运行后,稍等片刻,若控制台成功打印从 Redis 获取的用户信息,则表示 Spring Boot 已成功集成 Redis。   完善 Web 框架统一响应处理为什么需要统一响应? 默认接口返回的数据结构仅包含业务数据,缺少状态码和提示信息,无法清晰表达操作结果。通过统一封装响应结构,可以提升接口的规范性,便于前后端协同开发和快速定位问题。 下图展示了不规范与规范响应数据的对比:左侧是默认返回的非标准数据,右侧是统一封装后的规范数据。   定义统一业务状态码 在 com.youlai.boot.common.result 包下创建 ResultCode 枚举,错误码规范参考 阿里开发手册-错误码设计。 package com.youlai.boot.common.result; import java.io.Serializable;import lombok.Getter; /** * 统一业务状态码枚举 * * @author youlai */@Getterpublic enum ResultCode implements Serializable {     SUCCESS("00000", "操作成功"),    TOKEN_INVALID("A0230", "Token 无效或已过期"),    ACCESS_UNAUTHORIZED("A0301", "访问未授权"),    SYSTEM_ERROR("B0001", "系统错误");     private final String code;    private final String message;     ResultCode(String code, String message) {        this.code = code;        this.message = message;    }} 创建统一响应结构 定义 Result 类,封装响应码、消息和数据。 package com.youlai.boot.common.result; import lombok.Data;import java.io.Serializable; /** * 统一响应结构 * * @author youlai **/@Datapublic class Result<T> implements Serializable {    // 响应码    private String code;    // 响应数据    private T data;    // 响应信息    private String msg;     /**     * 成功响应     */    public static <T> Result<T> success(T data) {        Result<T> result = new Result<>();        result.setCode(ResultCode.SUCCESS.getCode());        result.setMsg(ResultCode.SUCCESS.getMsg());        result.setData(data);        return result;    }     /**     * 失败响应     */    public static <T> Result<T> failed(ResultCode resultCode) {        Result<T> result = new Result<>();        result.setCode(resultCode.getCode());        result.setMsg(resultCode.getMsg());        return result;    }     /**     * 失败响应(系统默认错误)     */    public static <T> Result<T> failed() {        Result<T> result = new Result<>();        result.setCode(ResultCode.SYSTEM_ERROR.getCode());        result.setMsg(ResultCode.SYSTEM_ERROR.getMsg());        return result;    } }封装接口返回结果 调整接口代码,返回统一的响应格式。 @Operation(summary = "获取用户详情")@GetMapping("/{id}")public Result<SysUser> getUserById(    @Parameter(description = "用户ID") @PathVariable Long id) {    SysUser user = userService.getById(id);    return Result.success(user);} 效果预览 接口返回结构变为标准格式:   通过以上步骤,接口响应数据已完成统一封装,具备良好的规范性和可维护性,有助于前后端协同开发与错误定位。 全局异常处理为什么需要全局异常处理 如果没有统一的异常处理机制,抛出的业务异常和系统异常会以非标准格式返回,给前端的数据处理和问题排查带来困难。为了规范接口响应数据格式,需要引入全局异常处理。 以下接口模拟了一个业务逻辑中的异常: @Operation(summary = "获取用户详情")@GetMapping("/{id}")public Result<SysUser> getUserById(    @Parameter(description = "用户ID") @PathVariable Long id) {    // 模拟异常    int i = 1 / 0;     SysUser user = userService.getById(id);    return Result.success(user);} 当发生异常时,默认返回的数据格式如下所示:   这类非标准的响应格式既不直观,也不利于前后端协作。 全局异常处理器 在 com.youlai.boot.common.exception 包下创建全局异常处理器,用于捕获和处理系统异常。 package com.youlai.boot.common.exception; import com.youlai.boot.common.result.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理器 * * @author youlai */@RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {     /**     * 处理系统异常     * <p>     * 兜底异常处理,处理未被捕获的异常     */    @ExceptionHandler(Exception.class)    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)    public <T> Result<T> handleNullPointerException(Exception e) {        log.error(e.getMessage(), e);        return Result.failed("系统异常:" + e.getMessage());    } } 验证全局异常处理 再次访问用户接口 localhost:8080/users/1 ,可以看到响应已经包含状态码和提示信息,数据格式变得更加规范:   自定义业务异常 在实际开发中,可能需要对特定的业务异常进行处理。通过自定义异常类 BusinessException,可以实现更灵活的异常处理机制。 package com.youlai.boot.common.exception; import com.youlai.boot.common.result.ResultCode;import lombok.Getter; /** * 自定义业务异常 * * @author youlai */@Getterpublic class BusinessException extends RuntimeException {     public ResultCode resultCode;     public BusinessException(ResultCode errorCode) {        super(errorCode.getMsg());        this.resultCode = errorCode;    }     public BusinessException(String message) {        super(message);    } } 在全局异常处理器中添加业务异常处理逻辑 @RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {     /**     * 处理自定义业务异常     */    @ExceptionHandler(BusinessException.class)    public <T> Result<T> handleBusinessException(BusinessException e) {        log.error(e.getMessage(), e);        if(e.getResultCode()!=null){            return Result.failed(e.getResultCode());        }        return Result.failed(e.getMessage());    } } 模拟业务异常     @Operation(summary = "获取用户详情")    @GetMapping("/{id}")    public Result<SysUser> getUserById(            @Parameter(description = "用户ID") @PathVariable Long id    ) {        SysUser user = userService.getById(-1);        // 模拟异常        if (user == null) {            throw new BusinessException("用户不存在");        }        return Result.success(user);    }  请求不存在的用户时,响应如下:   通过全局异常处理的引入和自定义业务异常的定义,接口的响应数据得以标准化,提升了前后端协作的效率和系统的可维护性。 日志输出配置日志作为企业级应用项目中的重要一环,不仅是调试问题的关键手段,更是用户问题排查和争议解决的强有力支持工具 配置 logback-spring.xml 日志文件 在 src/main/resources 目录下,新增 logback-spring.xml 配置文件。基于 Spring Boot “约定优于配置” 的设计理念,项目默认会自动加载并使用该配置文件。 <?xml version="1.0" encoding="UTF-8"?><!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 --><configuration>     <!-- SpringBoot默认logback的配置 -->    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>     <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>    <property name="LOG_HOME" value="/logs/${APP_NAME}"/>     <!-- 1. 输出到控制台-->    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">        <!-- <withJansi>true</withJansi>-->        <!--此日志appender是为开发使用,只配置最低级别,控制台输出的日志级别是大于或等于此级别的日志信息-->        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">            <level>DEBUG</level>        </filter>        <encoder>            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>            <charset>UTF-8</charset>        </encoder>    </appender>     <!-- 2. 输出到文件  -->    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">        <!-- 当前记录的日志文档完整路径 -->        <file>${LOG_HOME}/log.log</file>        <encoder>            <!--日志文档输出格式-->            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n</pattern>            <charset>UTF-8</charset>        </encoder>        <!-- 日志记录器的滚动策略,按大小和时间记录 -->        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">            <!-- 滚动后的日志文件命名模式 -->            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.log</fileNamePattern>            <!-- 单个日志文件的最大大小 -->            <maxFileSize>10MB</maxFileSize>            <!-- 最大保留30天的日志 -->            <maxHistory>30</maxHistory>            <!-- 总日志文件大小不超过3GB -->            <totalSizeCap>1GB</totalSizeCap>        </rollingPolicy>        <!-- 临界值过滤器,输出大于INFO级别日志 -->        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">            <level>INFO</level>        </filter>    </appender>     <!-- 根日志记录器配置 -->    <root level="INFO">        <!-- 引用上面定义的两个appender,日志将同时输出到控制台和文件 -->        <appender-ref ref="CONSOLE"/>        <appender-ref ref="FILE"/>    </root></configuration> 查看日志输出效果 添加配置文件后,启动项目并触发相关日志行为,控制台和日志文件会同时输出日志信息:   集成 Spring SecuritySpring Security 是一个强大的安全框架,可用于身份认证和权限管理。 添加依赖在 pom.xml 添加 Spring Security 依赖 <dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-security</artifactId></dependency> 获取用户认证信息从数据库获取用户信息(用户名、密码、角色),用于和前端输入的用户名密码做判读,如果认证成功,将角色权限信息绑定到用户会话,简单概括就是提供给认证授权的用户信息。 定义用户认证信息类 UserDetails 创建 com.youlai.boot.security.model 包,新建 SysUserDetails 用户认证信息对象,继承 Spring Security 的 UserDetails 接口 /** * Spring Security 用户认证信息对象 * <p> * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 * * @author youlai */@Data@NoArgsConstructorpublic class SysUserDetails implements UserDetails {     /**     * 用户ID     */    private Integer userId;     /**     * 用户名     */    private String username;     /**     * 密码     */    private String password;     /**     * 账号是否启用(true:启用,false:禁用)     */    private Boolean enabled;     /**     * 用户角色权限集合     */    private Collection<SimpleGrantedAuthority> authorities;     /**     * 根据用户认证信息初始化用户详情对象     */    public SysUserDetails(SysUser user) {        this.userId = user.getId();        this.username = user.getUsername();        this.password = user.getPassword();        this.enabled = ObjectUtil.equal(user.getStatus(), 1);         // 初始化角色权限集合        this.authorities = CollectionUtil.isNotEmpty(user.getRoles())                ? user.getRoles().stream()                // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (sys:user:add)                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))                .collect(Collectors.toSet())                : Collections.emptySet();    }     @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        return this.authorities;    }     @Override    public String getPassword() {        return this.password;    }     @Override    public String getUsername() {        return this.username;    }     @Override    public boolean isEnabled() {        return this.enabled;    }} 获取用户认证信息服务类 创建 com.youlai.boot.security.service 包,新建 SysUserDetailsService 用户认证信息加载服务类,继承 Spring Security 的 UserDetailsService 接口 /** * 用户认证信息加载服务类 * <p> * 在用户登录时,Spring Security 会自动调用该类的 {@link #loadUserByUsername(String)} 方法, * 获取封装后的用户信息对象 {@link SysUserDetails},用于后续的身份验证和权限管理。 * * @author youlai */@Service@RequiredArgsConstructorpublic class SysUserDetailsService implements UserDetailsService {     private final SysUserService userService;     /**     * 根据用户名加载用户的认证信息     */    @Override    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        // 查询用户基本信息        SysUser user = userService.getOne(new LambdaQueryWrapper<SysUser>()                .eq(SysUser::getUsername, username)        );        if (user == null) {            throw new UsernameNotFoundException(username);        }        // 模拟设置角色,实际应从数据库获取用户角色信息        Set<String> roles = Set.of("ADMIN");        user.setRoles(roles);         // 模拟设置权限,实际应从数据库获取用户权限信息        Set<String> perms = Set.of("sys:user:query");        user.setPerms(perms);         // 将数据库中查询到的用户信息封装成 Spring Security 需要的 UserDetails 对象        return new SysUserDetails(user);    }} 认证鉴权异常处理在 com.youlai.boot.common.util 添加响应工具类 ResponseUtils @Slf4jpublic class ResponseUtils {     /**     * 异常消息返回(适用过滤器中处理异常响应)     *     * @param response  HttpServletResponse     * @param resultCode 响应结果码     */    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {        // 根据不同的结果码设置HTTP状态        int status = switch (resultCode) {            case ACCESS_UNAUTHORIZED,             ACCESS_TOKEN_INVALID                     -> HttpStatus.UNAUTHORIZED.value();            default -> HttpStatus.BAD_REQUEST.value();        };         response.setStatus(status);        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        response.setCharacterEncoding(StandardCharsets.UTF_8.name());         try (PrintWriter writer = response.getWriter()) {            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));            writer.print(jsonResponse);            writer.flush(); // 确保将响应内容写入到输出流        } catch (IOException e) {            log.error("响应异常处理失败", e);        }    } } 功能 AuthenticationEntryPoint AccessDeniedHandler对应异常 AuthenticationException AccessDeniedException适用场景 用户未认证(无凭证或凭证无效) 用户已认证但无权限返回 HTTP 状态码 401 Unauthorized 403 Forbidden常见使用位置 用于处理身份认证失败的全局入口逻辑 用于处理权限不足时的逻辑用户未认证处理器 /** * 未认证处理器 * * @author youlai */@Slf4jpublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {     @Override    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {        if (authException instanceof BadCredentialsException) {            // 用户名或密码错误            ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR);        } else {            // token 无效或者 token 过期            ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);        }    }    } 无权限访问处理器 /** * 无权限访问处理器 * * @author youlai */public class MyAccessDeniedHandler implements AccessDeniedHandler {     @Override    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {        ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);    } } 注意事项 在全局异常处理器中,认证异常(AuthenticationException)和授权异常(AccessDeniedException)不应被捕获,否则这些异常将无法交给 Spring Security 的异常处理机制进行处理。因此,当捕获到这类异常时,应该将其重新抛出,交给 Spring Security 来处理其特定的逻辑。 public class GlobalExceptionHandler {     /**     * 处理系统异常     * <p>     * 兜底异常处理,处理未被捕获的异常     */    @ExceptionHandler(Exception.class)    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)    public <T> Result<T> handleNullPointerException(Exception e) throws Exception {        // 如果是 Spring Security 的认证异常或授权异常,直接抛出,交由 Spring Security 的异常处理器处理        if (e instanceof AccessDeniedException                || e instanceof AuthenticationException) {            throw e;        }         log.error(e.getMessage(), e);        return Result.failed("系统异常,请联系管理员");    } } 认证授权配置在 com.youlai.boot.config 包下新建 SecurityConfig 用来 Spring Security 安全配置 /** * Spring Security 安全配置 * * @author youlai */@Configuration@EnableWebSecurity  // 启用 Spring Security 的 Web 安全功能,允许配置安全过滤链@EnableMethodSecurity // 启用方法级别的安全控制(如 @PreAuthorize 等)public class SecurityConfig {     /**     * 忽略认证的 URI 地址     */    private final String[] IGNORE_URIS = {"/api/v1/auth/login"};     /**     * 配置安全过滤链,用于定义哪些请求需要认证或授权     */    @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {         // 配置认证与授权规则        http                .authorizeHttpRequests(requestMatcherRegistry ->                        requestMatcherRegistry                                .requestMatchers(IGNORE_URIS).permitAll() // 登录接口无需认证                                .anyRequest().authenticated() // 其他请求必须认证                )                // 使用无状态认证,禁用 Session 管理(前后端分离 + JWT)                .sessionManagement(configurer ->                        configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)                )                // 禁用 CSRF 防护(前后端分离通过 Token 验证,不需要 CSRF)                .csrf(AbstractHttpConfigurer::disable)                // 禁用默认的表单登录功能                .formLogin(AbstractHttpConfigurer::disable)                // 禁用 HTTP Basic 认证(统一使用 JWT 认证)                .httpBasic(AbstractHttpConfigurer::disable)                // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中                .headers(headers ->                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)                )                // 异常处理                .exceptionHandling(configurer -> {                    configurer                            .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证处理器                            .accessDeniedHandler(new MyAccessDeniedHandler()); // 无权限访问处理器                });                        ;         return http.build();    }     /**     * 配置密码加密器     *     * @return 密码加密器     */    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }         /**     * 用于配置不需要认证的 URI 地址     */    @Bean    public WebSecurityCustomizer webSecurityCustomizer() {        return (web) -> {            web.ignoring().requestMatchers(                    "/v3/api-docs/**",                    "/swagger-ui/**",                    "/swagger-ui.html",                    "/webjars/**",                    "/doc.html"            );        };    }     /**     *认证管理器     */    @Bean    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {        return configuration.getAuthenticationManager();    }} Token 工具类在 com.youlai.boot.security.manager 包下新建 JwtTokenManager ,用于生成和解析 token /** * JWT Token 管理类 * * @author youlai */@Servicepublic class JwtTokenManager {     /**     * JWT 密钥,用于签名和解签名     */    private final String secretKey = " SecretKey012345678901234567890123456789012345678901234567890123456789";     /**     * 访问令牌有效期(单位:秒), 默认 1 小时     */    private final Integer accessTokenTimeToLive = 3600;     /**     *  生成 JWT 访问令牌 - 用于登录认证成功后生成 JWT Token     *     * @param authentication 用户认证信息     * @return JWT 访问令牌     */    public String generateToken(Authentication authentication) {        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();        Map<String, Object> payload = new HashMap<>();        // 将用户 ID 放入 JWT 载荷中, 如有其他扩展字段也可以放入        payload.put("userId", userDetails.getUserId());         // 将用户的角色和权限信息放入 JWT 载荷中,例如:["ROLE_ADMIN", "sys:user:query"]        Set<String> authorities = authentication.getAuthorities().stream()                .map(GrantedAuthority::getAuthority)                .collect(Collectors.toSet());        payload.put("authorities", authorities);         Date now = new Date();        payload.put(JWTPayload.ISSUED_AT, now);         // 设置过期时间 -1 表示永不过期        if (accessTokenTimeToLive != -1) {            Date expiresAt = DateUtil.offsetSecond(now, accessTokenTimeToLive);            payload.put(JWTPayload.EXPIRES_AT, expiresAt);        }        payload.put(JWTPayload.SUBJECT, authentication.getName());        payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());         return JWTUtil.createToken(payload, secretKey.getBytes());    }      /**     * 解析 JWT Token 获取 Authentication 对象 - 用于接口请求时解析 JWT Token 获取用户信息     *     * @param token JWT Token     * @return Authentication 对象     */    public Authentication parseToken(String token) {         JWT jwt = JWTUtil.parseToken(token);        JSONObject payloads = jwt.getPayloads();        SysUserDetails userDetails = new SysUserDetails();        userDetails.setUserId(payloads.getInt("userId")); // 用户ID        userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名        // 角色集合        Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray("authorities")                .stream()                .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))                .collect(Collectors.toSet());         return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);    }     /**     *  验证 JWT Token 是否有效     *     * @param token JWT Token 不携带 Bearer 前缀     * @return 是否有效     */    public boolean validateToken(String token) {        JWT jwt = JWTUtil.parseToken(token);        // 检查 Token 是否有效(验签 + 是否过期)        return jwt.setKey(secretKey.getBytes()).validate(0);    } } 登录认证接口在 com.youlai.boot.controller 包下新建 AuthController /** * 认证控制器 * * @author youlai */@Tag(name = "01.认证中心")@RestController@RequestMapping("/api/v1/auth")@RequiredArgsConstructorpublic class AuthController {     // 认证管理器 - 用于执行认证    private final AuthenticationManager authenticationManager;     // JWT 令牌服务类 - 用于生成 JWT 令牌    private final JwtTokenManager jwtTokenManager;     @Operation(summary = "登录")    @PostMapping("/login")    public Result<String> login(            @Parameter(description = "用户名", example = "admin") @RequestParam String username,            @Parameter(description = "密码", example = "123456") @RequestParam String password    ) {         // 1. 创建用于密码认证的令牌(未认证)        UsernamePasswordAuthenticationToken authenticationToken =                new UsernamePasswordAuthenticationToken(username.trim(), password);         // 2. 执行认证(认证中)        Authentication authentication = authenticationManager.authenticate(authenticationToken);         // 3. 认证成功后生成 JWT 令牌(已认证)        String accessToken = jwtTokenManager.generateToken(authentication);         return Result.success(accessToken);    }} 访问本地接口文档 http://localhost:8080/doc.html 选择登录接口进行调试发送请求,输入用户名和密码,如果登录成功返回访问令牌 token   访问 https://jwt.io/ 解析返回的 token ,主要分为三部分 Header(头部) 、Payload(负载) 和 Signature(签名) ,其中负载除了固定字段之外,还出现自定义扩展的字段 userId。   访问鉴权我们拿获取用户列表举例,首先需要验证我们在上一步登录拿到的访问令牌 token 是否有效(验签、是否过期等),然后需要校验该用户是否有访问接口的权限,本节就围绕以上问题展开。 验证解析 Token 过滤器 新建 com.youlai.boot.security.filter 添加 JwtValidationFilter 过滤器 用于验证和解析token /** * JWT Token 验证和解析过滤器 * <p> * 负责从请求头中获取 JWT Token,验证其有效性并将用户信息设置到 Spring Security 上下文中。 * 如果 Token 无效或解析失败,直接返回错误响应。 * </p> * * @author youlai */public class JwtAuthenticationFilter extends OncePerRequestFilter {     private static final String BEARER_PREFIX = "Bearer ";     private final JwtTokenManager jwtTokenManager;     public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {        this.jwtTokenManager = jwtTokenManager;    }     @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {        String token = request.getHeader(HttpHeaders.AUTHORIZATION);        try {            if (StrUtil.isNotBlank(token) && token.startsWith(BEARER_PREFIX)) {                // 去除 Bearer 前缀                token = token.substring(BEARER_PREFIX.length());                // 校验 JWT Token ,包括验签和是否过期                boolean isValidate = jwtTokenService.validateToken(token);                if (!isValidate) {                    writeErrMsg(response, ResultCode.TOKEN_INVALID);                    return;                }                // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中                Authentication authentication = jwtTokenManager.parseToken(token);                SecurityContextHolder.getContext().setAuthentication(authentication);            }        } catch (Exception e) {            SecurityContextHolder.clearContext();            writeErrMsg(response, ResultCode.TOKEN_INVALID);            return;        }                // 无 Token 或 Token 验证通过时,继续执行过滤链。        // 如果请求不在白名单内(例如登录接口、静态资源等),        // 后续的 AuthorizationFilter 会根据配置的权限规则和安全策略进行权限校验。        // 例如:        // - 匹配到 permitAll() 的规则会直接放行。        // - 需要认证的请求会校验 SecurityContext 中是否存在有效的 Authentication。        // 若无有效 Authentication 或权限不足,则返回 403 Forbidden。        filterChain.doFilter(request, response);    }     /**     * 异常消息返回     *     * @param response  HttpServletResponse     * @param resultCode 响应结果码     */    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {        int status = switch (resultCode) {            case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();            default -> HttpStatus.BAD_REQUEST.value();        };         response.setStatus(status);        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        response.setCharacterEncoding(StandardCharsets.UTF_8.name());         try (PrintWriter writer = response.getWriter()) {            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));            writer.print(jsonResponse);            writer.flush();        } catch (IOException e) {            // 日志记录:捕获响应写入失败异常            // LOGGER.error("Error writing response", e);        }    }} 添加 JWT 验证和解析过滤器 在 SecurityConfig 过滤器链添加 JWT token校验和解析成 Authentication 对象的过滤器。 /** * Spring Security 安全配置 * * @author youlai */@RequiredArgsConstructorpublic class SecurityConfig {     // JWT Token 服务 , 用于 Token 的生成、解析、验证等操作    private final JwtTokenManager jwtTokenManager;     @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {      return http                 // ...                 // JWT 验证和解析过滤器                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class)  .build();    }     // ...} 获取用户列表接口 @RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController {     private final SysUserService userService;     @Operation(summary = "获取用户列表")    @GetMapping    @PreAuthorize("hasAuthority('sys:user:query')")    public List<SysUser> listUsers() {        return userService.list();    }    }访问一个主要测试访问凭据令牌是否认证以及对应的用户是否有访问该接口所需的权限,上面获取用户信息列表的接口未配置在security的白名单中,也就是需要认证,且被 @PreAuthorize(“hasAuthority(‘sys:user:query’)”) 标记说明用户需要有 sys:user:query的权限,也就是所谓的鉴权。 正常访问  不携带 token 访问   携带错误/过期的 token   有访问权限 用户拥有的权限 sys:user:query public class UserController {     @Operation(summary = "获取用户列表")    @GetMapping    @PreAuthorize("hasAuthority('sys:user:query')")  // 需要 sys:user:query 权限    public List<SysUser> listUsers() {        return userService.list();    }    }   无访问权限 用户没有拥有的权限 sys:user:info———————————————— 原文链接:https://blog.csdn.net/u013737132/article/details/145177011
  • [技术干货] 详解Java之Spring MVC篇二
    理解CookieHTTP协议自身是“无状态”协议,但是在实际开发中,我们很多时候是需要知道请求之间的关联关系的。 上述图中的 "令牌" 通常就存储在 Cookie 字段中.此时在服务器这边就需要记录"令牌"信息, 以及令牌对应的⽤⼾信息, 这个就是 Session 机制所做的⼯作.理解Session我们先来了解⼀下什么是会话.会话: 对话的意思 在计算机领域, 会话是⼀个客⼾与服务器之间的不中断的请求响应. 对客⼾的每个请求,服务器能够识别出请求来⾃于同⼀个客⼾. 当⼀个未知的客⼾向Web应⽤程序发送第⼀个请求时就开始了⼀个会话.当客⼾明确结束会话或服务器在⼀个时限内没有接受到客⼾的任何请求时,会话就结束了.服务器同⼀时刻收到的请求是很多的. 服务器需要清楚的区分每个请求是从属于哪个⽤⼾, 也就是属于哪个会话, 就需要在服务器这边记录每个会话以及与⽤⼾的信息的对应关系.Session是服务器为了保存⽤⼾信息⽽创建的⼀个特殊的对象. Session的本质就是⼀个 "哈希表", 存储了⼀些键值对结构. Key 就是SessionID, Value 就是⽤⼾信息(⽤⼾信息可以根据需求灵活设计). SessionId是由服务器⽣成的⼀个 "唯⼀性字符串", 从 Session 机制的⻆度来看, 这个唯⼀性字符串称为 "SessionId". 但是站在整个登录流程中看待, 也可以把这个唯⼀性字符串称为 "token".上述例⼦中的令牌ID, 就可以看做是SessionId, 只不过令牌除了ID之外, 还会带⼀些其他信息, ⽐如时间, 签名等. 1. 当⽤⼾登陆的时候, 服务器在 Session 中新增⼀个新记录, 并把 sessionId返回给客⼾端. (通过HTTP 响应中的 Set-Cookie 字段返回).2. 客⼾端后续再给服务器发送请求的时候, 需要在请求中带上 sessionId. (通过 HTTP 请求中的Cookie 字段带上).3. 服务器收到请求之后, 根据请求中的 sessionId在 Session 信息中获取到对应的⽤⼾信息, 再进⾏后续操作.找不到则重新创建Session, 并把SessionID返回. Session 默认是保存在内存中的. 如果重启服务器则 Session 数据就会丢失.Cookie和Session的区别• Cookie 是客⼾端保存⽤⼾信息的⼀种机制. Session 是服务器端保存⽤⼾信息的⼀种机制.• Cookie 和 Session之间主要是通过 SessionId 关联起来的, SessionId 是 Cookie 和 Session 之间的桥梁• Cookie 和 Session 经常会在⼀起配合使⽤. 但是不是必须配合.• 完全可以⽤ Cookie 来保存⼀些数据在客⼾端. 这些数据不⼀定是⽤⼾⾝份信息, 也不⼀定是SessionId• Session 中的sessionId 也不需要⾮得通过 Cookie/Set-Cookie 传递, ⽐如通过URL传递.1. 存储位置Cookie:存储在客户端(浏览器)上。当服务器响应一个HTTP请求时,它可以在响应头中包含一个Set-Cookie字段,浏览器会保存这个Cookie,并在后续的请求中通过Cookie请求头将Cookie发送回服务器。Session:存储在服务器端。服务器为每个用户会话创建一个唯一的标识符(通常是Session ID),这个标识符被发送到客户端(通常是通过Cookie,但也可以通过URL重写等方式),客户端在后续的请求中携带这个标识符,服务器通过这个标识符来识别用户会话。2. 安全性Cookie:由于存储在客户端,因此相对容易受到攻击,如跨站脚本攻击(XSS)可以读取或修改Cookie。但是,可以通过设置HttpOnly和Secure标志来增加安全性,HttpOnly标志可以防止JavaScript访问Cookie,Secure标志则要求Cookie仅通过HTTPS发送。Session:存储在服务器端,因此相对更安全。但是,如果Session ID被泄露(例如,通过URL重写并泄露在日志中),则可能面临会话劫持的风险。3. 容量限制Cookie:由于存储在客户端,其大小受到浏览器和服务器设置的限制。大多数浏览器对每个Cookie的大小和每个域名下的Cookie总数都有限制。Session:存储在服务器端,因此其大小限制主要取决于服务器的内存和配置,通常远大于Cookie的限制。4. 生命周期Cookie:可以设置过期时间(Expires/Max-Age),也可以不设置(会话Cookie,浏览器关闭时失效)。Session:通常依赖于服务器端的配置和Session的存储方式(如内存、数据库等)。如果服务器配置了Session的超时时间,则Session在达到超时时间后会被销毁。5. 使用场景Cookie:适用于存储少量数据,如用户偏好设置、登录状态等。由于存储在客户端,可以跨多个页面和请求持久化数据。Session:适用于存储大量数据,如用户信息、购物车内容等。由于存储在服务器端,可以更安全地管理用户会话。获取Cookie首先先设置Cookie 再获取:    @RequestMapping("/getC")    public String getCookie(HttpServletRequest request){        //获取参数//        String name = request.getParameter("name");        Cookie[] cookies = request.getCookies();        if (cookies!=null){            Arrays.stream(cookies).forEach(ck -> System.out.println(ck.getName()+":"+ck.getValue()));        }        return "获取Cookie";    }更为简洁的代码:@RequestMapping("/getC2")public String getCookie2(@CookieValue("name") String name){    return "从Cookie中获取值, name:"+name;}获取SessionSession是服务器端的机制, 我们需要先存储, 才能再获取.Session 也是基于HttpServletRequest 来存储和获取的.设置Session    @RequestMapping("/setSess")    public String setSess(HttpServletRequest request){        //从cookie中获取到了sessionID, 根据sessionID获取Session对象, 如果没有获取到, 会创建一个session对象        HttpSession session = request.getSession();        session.setAttribute("name", "zhangsan");        return "设置session成功";    }再获取:    @RequestMapping("/getSess")    public String getSess(HttpServletRequest request){        //从cookie中获取到了sessionID, 根据session获取Session对象        HttpSession session = request.getSession();        String name = (String)session.getAttribute("name");        return "从session中获取name:"+name;    } 获取Session更为简洁的代码:    @RequestMapping("/getSess2")    public String getSess2(HttpSession session){        String name = (String)session.getAttribute("name");        return "从session中获取name:"+name;    }    @RequestMapping("/getSess3")    public String getSess3(@SessionAttribute("name") String name){        return "从session中获取name:"+name;    }获取Header获取User-Agent    @RequestMapping("/getHeader")    public String getHeader(HttpServletRequest request){        String userAgent = request.getHeader("User-Agent");        return "从header中获取信息, userAgent:"+userAgent;     }更为简洁的代码:    @RequestMapping("/getHeader2")    public String getHeader2(@RequestHeader("User-Agent") String userAgent){        return "从header中获取信息, userAgent:"+userAgent; ————————————————                 原文链接:https://blog.csdn.net/wmh_1234567/article/details/141640649
  • [技术干货] Java篇图书管理系统
    前言相信大家都有去过图书馆吧,那么在借阅图书和归还的时候,都有一个系统来记录并操作这些过程,所以今天就带着大家利用之前的所学知识来简单实现一下图书管理系统的基本逻辑构造一. 图书管理系统的核心图书管理系统的核心包括三个部分:书籍的信息(书本属性)、操作书籍的人(管理员和读者)、 对书籍的操作(借阅、归还)所以在这里我们分别创建3个包 book、operation、user 来实现各个部分的操作: 二. 图书管理系统基本框架2.1 book包我们在book这个包中创建2个类:Book、Booklist,Book用来描述书籍的基本信息,Booklist充当书架,里面用来记录书架中书籍的信息。 2.1.1 Book(书籍类)首先在Book中我们要创建变量来记录书籍的基本信息:    private String name; //书名    private String author; //作者    private  int price;  //价格    private String type; //书籍类型    private boolean isborrow; //是否被借出并且这里的书籍基本信息,我们是用private访问修饰限定符修饰变量的,其他类想要直接访问是访问不了的,所以这里需要我们创建方法间接访问。public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getAuthor() {        return author;    }    public void setAuthor(String author) {        this.author = author;    }    public int getPrice() {        return price;    }    public void setPrice(int price) {        this.price = price;    }    public String getType() {        return type;    }    public void setType(String type) {        this.type = type;    }    public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    }接下来我们来写Book类的构造方法:那么当我们每次新增一本书的时候 ,该书籍默认就是未借出的状态,默认初始值就是false,所以不需要在构造方法中进行初始化public Book(String name, String author, int price, String type) {        //初始化书的属性        this.name = name;        this.author = author;        this.price = price;        this.type = type;    }这时候书籍的信息属性都能够被记录了,那么我们想看看这些书籍的信息该怎么办呢?没错打印出来,此时我们需要对 ToString 方法进行重写:  2.1.2 Booklist (书架类)能够记录书籍信息的Book类创建完成后,我们来创建一个书架类(Booklist),书架类的作用就是能够存放每一本书,并且记录每一本书的借阅状态。    private Book [] books;//表示书架可以存放10本书    private int usedbooks;//表示books数组中存放了几本书那么我们依然需要通过间接访问才能访问书架中的基本信息,需要自己提供方法间接访问:    public int getUsedbooks() {        return usedbooks;//返回有效书本数量    }    public void setUsedbooks(int usedbooks) {        this.usedbooks = usedbooks;//更改有效书本数量    }    public Book getbook(int pos){        return books[pos];  //返回书架中 第pos本书    }    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }那么我们还是写一下Booklist类的构造方法,默认书架中已经存放了书籍:public BookList() {        this.books = new Book [10];  //默认创建存放10本书大小的数组        this.books[0]=new Book("三国演义","罗贯中",20,"四大名著");        this.books[1]=new Book("西游记","吴承恩",22,"四大名著");        this.books[2]=new Book("红楼梦","曹雪芹",24,"四大名著");        this.books[3]=new Book("水浒传","施耐庵",26,"四大名著");        this.usedbooks=4;//初始化有效书本数量为4本    } 那么在book这个包的操作就告一段落,后续随着思路的深入再进行补充~2.2 user包在user包中我们新建三个类来描述管理员和用户,那么管理员和用户也是拥有相同属性的,而第三个类User,我们就定义成抽象类,让管理员类与用户类去继承,这样就可以省略一些重复的代码啦2.2.1 User类那么管理员与用户共同拥有的特征就是都有名字,那么我们就在User类中定义一个名字属性的变量,并且通过构造方法初始化 管理员 或 用户 的名称:public abstract class User {     public String name; //名字     public User(String name) {        this.name = name; //初始化名称    }}2.2.2  Administrator(管理员类)因为我们的管理员类继承了User类,所以需要在构造方法中调用父类的构造方法:public class Administrator extends User{    public Administrator(String name) {        super(name);//通过父类初始化管理员名称    }}2.2.3 Visitor(用户类)同理用户类也继承了User类,所以也需要构造方法调用父类的构造方法:public class Visitor extends User{    public Visitor(String name) {        super(name); //通过父类初始化用户名称    }}那么书籍的属性和操作者的基本框架我们已经实现了,接下来该实现一下图书管理系统的功能了~2.3 图书管理系统操作菜单管理员菜单1.查找图书2.新增图书3.删除图书4.显示图书0.退出系统用户菜单1.查找图书2.借阅图书3.归还图书0.退出系统接下来,我们就针对这些功能创建一个包,实现这些功能。2.4 operation(操作包)在这个包中我们去实现上述描述的相关操作,接着我们创建一个接口来实现多态,从而降低代码的复杂度operate接口:public interface Operate {  //服务接口    void work(BookList books);    //功能的实现我们都要利用到书架,所以这里参数给定一个书架对象}接下来我们创建实现各个功能的类: Addbook类:(新增书籍)public class Addbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("增加书籍...");    }}Borrowbook类:(借阅书籍)public class Borrowbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("借阅书籍....");    }} Deletebook类:(删除书籍)public class Deletebook implements Operate{    @Override    public void work(BookList books) {        System.out.println("删除书籍...");    }}Exitbook类:(退出系统)public class Exitbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("退出系统.....");    }}Findbook类:(查阅书籍)public class Findbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("查找书籍....");    ]}Returnbook类:(归还书籍)public class Retuenbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("归还书籍....");Showbook类:(显示书籍)public class Showbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("显示书籍....");    }}接下来就要慢慢的去将每个类的功能具体的实现,并且将每个类联系起来运作图书管理系统。既然知道了管理员和用户的菜单后,我们就分别给管理员和用户添加一下菜单:在Administrator类添加管理员菜单:public void menu() {        System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统");        System.out.println("<<**************管理员操作菜单******************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 新增图书 ");        System.out.println("3. 删除图书 ");        System.out.println("4. 显示图书");        System.out.println("0. 退出系统 ");        System.out.println("<<******************************************>>");}在Visitor类中添加用户菜单:public void menu(){        System.out.println("欢迎 "+this.name+" 读者来到图书管理系统");        System.out.println("<<***************游客读者菜单*************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 借阅图书 ");        System.out.println("3. 归还图书 ");        System.out.println("0. 退出系统 ");        System.out.println("<<**************************************>>");} 那么我们现在创建一个Main类,在类中写上我们的main方法实现一下基本逻辑:​public class Main {    public static User login(){   //登录系统过程        Scanner scanner=new Scanner(System.in);        System.out.println("请输入你的名字:");        String name = scanner.nextLine();         System.out.println("请输入你要登入的帐号:1.管理员登录 -----> 2.游客登录 ----->");        int choice=scanner.nextInt();        if(choice==1){            return new Administrator(name); //如果选择了1,就实例化一个管理员对象,并返回        }else{            return new Visitor(name); //如果选择了2,就实例化一个用户对象,并返回        }        //此时的返回值我们不能确定返回的是管理员对象还是用户对象,所以这里用向上转型返回User类型的对象    }      public static void main(String[] args) {            User user=login(); //进入登录系统            user.menu(); //打印菜单    }} ​但是此时login方法的返回对象我们使用User类型的对象进行接收的,此时我们是不能使用user.menu()的,那么该怎么办呢?我们之前将User抽象出来作为管理员类和用户类的父类,那么此时我们在User类中写一个menu()方法就可以实现多态,打印菜单啦~public abstract class User {    public String name; //用户名称     public User(String name) {        this.name = name; //初始化用户名称    }    public abstract int menu(); //抽象方法实现多态,打印菜单}那么此时只需要将子类做出相应的改动,改变成重写的形式就可以了: 这就实现了打印菜单的功能,但是光打印选择不了功能可不行呀,所以我们要在菜单中加上一个选择功能:  @Override    public int  menu() {        System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统");        System.out.println("<<**************管理员操作菜单******************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 新增图书 ");        System.out.println("3. 删除图书 ");        System.out.println("4. 显示图书");        System.out.println("0. 退出系统 ");        System.out.println("<<******************************************>>");        //选择功能        Scanner scanner=new Scanner(System.in);        System.out.println("请选择您需要的服务:-->>");        int choice=scanner.nextInt();        return choice;    }通过输入标号来选择我们想要的服务,那么我们应该返回这个标号,所以menu()方法的返回值也要改成 int 类型,Visitor类的menu()方法也是同理。那么能够选择服务后,我们应该根据对象(管理员或用户)选择该调用哪一个菜单方法:Administrator(管理员类):public Administrator(String name) {        super(name);//通过父类初始化管理员名称        this.operate=new Operate[]{  //创建 管理员账户提供的服务                new Exitbook(),                new Findbook(),                new Addbook(),                new Deletebook(),                new Showbook()        };    }Visitor(用户类):public class Visitor extends User{    public Visitor(String name) {        super(name); //通过父类初始化用户名称        this.operate=new Operate[]{   //创建 游客账户提供的服务                new Exitbook(),                new Findbook(),                new Borrowbook(),                new Retuenbook()        };    } 那么当我们分别在它们的构造方法中创建一个Operate类型的数组后,里面存放Operate接口实现的方法,那么在new一个管理员对象或者用户对象时,系统就会为这个数组分配内存: 那么我们需要在User类中加上这么一段代码利用动态绑定调用管理员菜单中的方法还是调用用户菜单中的方法:    public Operate []operate;  //创建服务数组,子类通过数组下标调用 各个服务     public void Dooperate(int choice,BookList books){        this.operate[choice].work(books);  //调用 游客/管理员 所选择的服务    }this.operate[ choice ]就是我们new的那个对象中构造方法中下标为 choice 的那个类,而后面的 .work(books)就是调用的对应类的work方法。那么现在我们在main方法中打印菜单的时候就需要有个变量来接收选择的服务啦,再通过new对应的对象调用上面写的 Dooperate方法调用所选择的服务。public static void main(String[] args) {            BookList bookList=new BookList(); //实例化书架            User user=login(); //进入登录系统            while(true) {                int chioce = user.menu(); //接收选择服务的选项                user.Dooperate(chioce, bookList); //调用 游客/管理员 提供的服务            }    }而为了实现用户输入0时才退出系统,所以这里的while循环我们设置为死循环。此时基本框架就搭建完成啦~我们来看看测试效果:那么我们来画图分析一下运行的过程:1. 通过主函数调用login方法: 2. 通过login方法返回的对象调用该对象类中的menu()方法 3. 最后通过调用user.Dooperate方法一步一步实现所选择的服务 三. 实现服务 3.1 显示所有书籍那么在实现显示书籍功能之前,我们应该在Booklist类中写一个方法让Showbook类中的work方法能够通过数组下标打印书籍的信息:    public Book getbook(int pos){        return this.books[pos];  //返回书架中 第pos本书    }接下来我们就可以实现显示书籍这个服务啦~在Showbook类中实现: public class Showbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("显示书籍....");        int count=books.getUsedbooks();        for(int i=0;i<count;i++) {            //通过循环遍历数组;调用books.getbook(i)方法获取指定的书籍对象            System.out.println(books.getbook(i).toString());        }        System.out.println();    }}3.2 查找书籍查找书籍的过程与显示所有书籍相似,也是通过循环遍历数组,利用equal()方法进行对比,实现服务。在Findbook类中实现:public class Findbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("查找书籍....");        Scanner scanner=new Scanner(System.in);        System.out.print("请输入要查找书籍的名字:-->");        String name=scanner.nextLine();        int count=books.getUsedbooks();        for(int i=0;i<count;i++)        {            Book book=books.getbook(i);            if(book.getName().equals(name)){                System.out.println("找到了,书本内容如下:");                System.out.println(book);                System.out.println();                return;            }        }        System.out.println("很抱歉,没有查找到你要查找的书籍");        System.out.println("后续会尽快联系管理员添加~");        System.out.println();    }} 3.3 退出系统在Exitbook类中实现:public class Exitbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("退出系统.....");        int count=books.getUsedbooks();        for(int i=0;i<count;i++)        {           books.setBooks(i,null); //        }        books.setUsedbooks(0); //将有效书籍数量 制为空        System.exit(0);    }}书架中存放的书籍都是对象,那么在退出系统之前我们应该将书籍对象都进行回收,避免发生内存泄漏,并且将书架中的书本数量变成0,最后在结尾加上 System.exit(0); 就可以结束程序,退出系统了。3.4 增加书籍在增加书籍之前,我们应该先判断这个书籍是否已经在书架中存放,如果书架中没有那么就添加到书架中在Addbook类中实现:public class Addbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("增加书籍...");        int count=books.getUsedbooks();        Scanner scanner=new Scanner(System.in);        System.out.print("请输入新添加的书名:--->");        String name=scanner.nextLine();         System.out.print("请输入新添加书籍的作者:--->");        String author=scanner.nextLine();         System.out.print("请输入书籍的价格:--->");        int price=scanner.nextInt();        scanner.nextLine();        System.out.print("请输入书籍的类型:--->");        String type=scanner.nextLine();         for(int i=0;i<count;i++) {           if(books.getbook(i).getName().equals(name)){               System.out.println("该书籍已经存在了,不需要添加-----");               System.out.println();               return ;           }        }        Book book=new Book(name,author,price,type);        books.setBooks(count,book);        System.out.println("添加成功---");        books.setUsedbooks(count+1);        System.out.println();    }}那么在Booklist类中我们就要提供一个方法用来新增书籍:    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }3.5 删除书籍在删除书籍中我们有两个点需要注意:当我们删除了某本书籍后,需要将该书籍后面的书籍往前移动,并且将移动完最后的那个空间进行回收,并且更改书架中书籍的数量在Deletebook类中实现:public class Deletebook implements Operate{    @Override    public void work(BookList books) {        System.out.println("删除书籍...");        System.out.print("请输入你要删除的书籍:--->");        Scanner scanner=new Scanner(System.in);        String name =scanner.nextLine();        int count=books.getUsedbooks();        int index=-1;        boolean flag=false;        for (int i = 0; i <count ; i++) {            if(books.getbook(i).getName().equals(name)){               index=i;               flag=true;               break;            }        }        if(flag==false) {            System.out.println("没有找到你要删除的书籍-----");            System.out.println();            return ;        }        if (books.getbook(index).isIsborrow()==true){            System.out.println("该书本已经被借出,暂时不能进行操作-----");            System.out.println();            return;        }else{            for(int j=index;j<count-1;j++)            {                books.setBooks(j,books.getbook(j+1));            }            System.out.println("删除成功------->");            books.setUsedbooks(count-1);            books.setBooks(count-1,null);        }    }}那么在删除书籍的时候为了实现将后面的书籍往前面移动,我们需要在Booklist类中调用setBooks方法来实现这个过程:    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }3.6 借阅书籍借阅书籍的时候我们应该判断该书籍是否在书架上存在,如果存在再判断该书籍是否被借出,如果没有被借出,那么就可以进行借阅在Borrowbook类中实现:ublic class Borrowbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("借阅书籍....");        System.out.print("请输入你要借阅的图书:-->");        Scanner scanner = new Scanner(System.in);        String name = scanner.nextLine();         int count = books.getUsedbooks();        for (int i = 0; i < count; i++) {            if (books.getbook(i).getName().equals(name)) {                if (books.getbook(i).isIsborrow() == false) {                    books.getbook(i).setIsborrow(true);                    System.out.println("借阅成功---");                    System.out.println(books.getbook(i));                    System.out.println();                    return;                }else{                    System.out.println("这本书已经被借出,请等待读者归还后再进行借阅---");                    System.out.println();                    return ;                }            }        }        System.out.println("查找不到你要借阅的书籍,后续会联系管理员尽快上架!!!");        System.out.println();    }}这个时候我们需要在Book类中提供一个方法,返回当前书籍是否被借出的状态,并且还要提供一个方法改变书籍是否被借出的状态,在Book类中实现:    public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    } 3.7 归还书籍在归还之前我们需要判断一下这本书是否在书架上存在过,如果存在再进行判断这本书是否有被借出,如果有被借出则可以归还在Returnbook类中实现: public class Retuenbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("归还书籍....");        System.out.print("请输入你要归还的图书:-->");        Scanner scanner = new Scanner(System.in);        String name = scanner.nextLine();         int count = books.getUsedbooks();        for (int i = 0; i < count; i++) {            if (books.getbook(i).getName().equals(name)) {                if (books.getbook(i).isIsborrow() == true) {                    books.getbook(i).setIsborrow(false);                    System.out.println("归还成功---欢迎再次借阅");                    System.out.println(books.getbook(i));                    System.out.println();                    return;                }else{                    System.out.println("这本书没有被借出,不需要归还---");                    System.out.println();                    return ;                }            }        }        System.out.println("查找不到你要归还的书籍,你可能是在其他图书馆借阅的书籍---");        System.out.println();    }}如果将书籍归还后我们需要将书籍是否借出的状态进行改变,在Book类中实现:public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    }四. 总代码Main类:import book.BookList;import user.Administrator;import user.User;import user.VipPerson;import user.Visitor;import java.util.Scanner; public class Main {    public static User login(){   //登录系统过程        Scanner scanner=new Scanner(System.in);        System.out.println("请输入你的名字:");        String name = scanner.nextLine();         System.out.println("请输入你要登入的帐号:1.管理员登录 -----> 2.游客登录 ----->");        int choice=scanner.nextInt();        if(choice==1){            return new Administrator(name); //如果选择了1,就创建一个管理员对象,并返回        }else if(choice==2){            return new Visitor(name); //如果选择了2,就创建一个游客对象,并返回        }else{            return new VipPerson(name);        }        //此时的返回值我们不能确定返回的是管理员对象还是用户对象,所以这里用向上转型返回User类型的对象    }    public static void main(String[] args) {            BookList bookList=new BookList(); //实例化书架            User user=login(); //进入登录系统            while(true) {                int chioce = user.menu(); //接收选择服务的选项                user.Dooperate(chioce, bookList); //调用 游客/管理员 提供的服务            }    }}User类:package user;import book.BookList;import operation.Operate; public abstract class User {    public String name; //用户名称     public Operate []operate;  //创建服务数组,子类通过数组下标调用 各个服务    public User(String name) {        this.name = name; //初始化用户名称    }    public abstract int menu(); //服务菜单    public void Dooperate(int choice,BookList books){        this.operate[choice].work(books);  //调用 游客/管理员 所选择的服务    }} Administrator类:package user;import operation.*;import java.util.Scanner;public class Administrator extends User{    public Administrator(String name) {        super(name);//通过父类初始化管理员名称        this.operate=new Operate[]{  //创建 管理员账户提供的服务                new Exitbook(),                new Findbook(),                new Addbook(),                new Deletebook(),                new Showbook()        };    }    @Override    public int  menu() {        System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统");        System.out.println("<<**************管理员操作菜单******************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 新增图书 ");        System.out.println("3. 删除图书 ");        System.out.println("4. 显示图书");        System.out.println("0. 退出系统 ");        System.out.println("<<******************************************>>");        //选择功能        Scanner scanner=new Scanner(System.in);        System.out.println("请选择您需要的服务:-->>");        int choice=scanner.nextInt();        return choice;    }}Visitor类:package user;import operation.*;import java.util.Scanner;public class Visitor extends User{    public Visitor(String name) {        super(name); //通过父类初始化用户名称        this.operate=new Operate[]{   //创建 游客账户提供的服务                new Exitbook(),                new Findbook(),                new Borrowbook(),                new Retuenbook()        };    }     public int menu(){        System.out.println("欢迎 "+this.name+" 读者来到图书管理系统");        System.out.println("<<***************游客读者菜单*************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 借阅图书 ");        System.out.println("3. 归还图书 ");        System.out.println("0. 退出系统 ");        System.out.println("<<**************************************>>");        Scanner scanner=new Scanner(System.in);        System.out.println("请选择您需要的服务:-->>");        int choice=scanner.nextInt();        return choice;    }}Book类:package book;public class Book {     //书的属性    private String name; //书名    private String author; //作者    private  int price;  //价格    private String type; //书籍类型    private boolean isborrow; //是否被借出    public Book(String name, String author, int price, String type) {        //初始化书的属性        this.name = name;        this.author = author;        this.price = price;        this.type = type;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getAuthor() {        return author;    }    public void setAuthor(String author) {        this.author = author;    }    public int getPrice() {        return price;    }    public void setPrice(int price) {        this.price = price;    }    public String getType() {        return type;    }    public void setType(String type) {        this.type = type;    }    public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    }     @Override    public String toString() {        return "Book{" +                "书名='" + name + '\'' +                ", 作者='" + author + '\'' +                ", 价格=" + price +                ", 类型='" + type + '\'' +                (isborrow==true?" ,已经借出":" ,未被借出") +                '}';    }}Booklist类:package book;public class BookList {    private Book [] books;//表示书架可以存放10本书    private int usedbooks;//表示books数组中存放了几本书     public BookList() {        this.books = new Book [10];  //默认创建存放10本书大小的数组        this.books[0]=new Book("三国演义","罗贯中",20,"四大名著");        this.books[1]=new Book("西游记","吴承恩",22,"四大名著");        this.books[2]=new Book("红楼梦","曹雪芹",24,"四大名著");        this.books[3]=new Book("水浒传","施耐庵",26,"四大名著");        this.usedbooks=4;//初始化有效书本数量为4本    }    public int getUsedbooks() {        return usedbooks;//返回有效书本数量    }    public void setUsedbooks(int usedbooks) {        this.usedbooks = usedbooks;//更改有效书本数量    }    public Book getbook(int pos){        return this.books[pos];  //返回书架中 第pos本书    }    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }} 那么operation这个包中各个类的代码都在 实现服务 那里写啦,这里就不在重新上传了~结语以上就是利用我们过去的所学知识实现的图书管理系统的基本逻辑构造,在此感谢大家的观看!!————————————————                  原文链接:https://blog.csdn.net/2402_86304740/article/details/143254119