当前位置: 首页 > news >正文

EasyExcel动态列导出

测试代码地址:https://gitee.com/wangtianwen1996/cento-practice/tree/master/src/test/java/com/xiaobai/easyexcel/dynamiccolumn
官方文档:https://easyexcel.opensource.alibaba.com/docs/2.x/quickstart/write

一、实现方式

1、根据需要导出的列找到返回类对象属性的ExcelPropertyColumnWidth注解,最终生成需要显示的列名和每列的列宽;
2、根据需要导出的列获取Excel中的行数据;
3、添加自定义单元格拦截策略(实现com.alibaba.excel.write.handler.WriteHandler接口)和数据类型转换策略(实现com.alibaba.excel.converters.Converter接口);
4、创建Excel的Sheet页,设置第一步获取的列宽;

二、代码实现

(一)添加基础数据类型转换器(LocalDate、LocalDateTime、LocalTime、Integer)

package com.xiaobai.easyexcel.dynamiccolumn;import cn.hutool.core.date.DateTime;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;/*** @author wangtw* @ClassName DataConverter* @description: 类型转换器* @date 2024/2/919:23*/
public class DataConverter {public static class CoreConverter<T> implements Converter<T> {private final Class<T> tClass;public CoreConverter(Class<T> tClass) {this.tClass = tClass;}/*** 导出支持的类型* @return*/@Overridepublic Class supportJavaTypeKey() {return tClass;}/*** 导入支持的Excel类型* @return*/@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}/*** 导入类型转换* @param cellData* @param excelContentProperty* @param globalConfiguration* @return* @throws Exception*/@Overridepublic T convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {if (cellData.getData() instanceof LocalDate) {return (T) LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));}if (cellData.getData() instanceof LocalTime) {return (T) LocalTime.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("HH:mm:ss"));}if (cellData.getData() instanceof LocalDateTime) {return (T) LocalDateTime.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}if (cellData.getData() instanceof Integer) {return (T) Integer.valueOf(cellData.getStringValue());}return null;}/*** 导出类型转换* @param obj* @param excelContentProperty* @param globalConfiguration* @return* @throws Exception*/@Overridepublic CellData convertToExcelData(T obj, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {if (obj instanceof LocalDate) {return new CellData(((LocalDate) obj).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));}if (obj instanceof LocalDateTime) {return new CellData(((LocalDateTime) obj).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));}if (obj instanceof LocalTime) {return new CellData(((LocalTime) obj).format(DateTimeFormatter.ofPattern("HH:mm:ss")));}if (obj instanceof Integer) {return new CellData(String.valueOf(obj));}return null;}}/*** localDate类型转换*/public static class LocalDateConverter extends CoreConverter<LocalDate> {public LocalDateConverter() {super(LocalDate.class);}}/*** localTime类型转换*/public static class LocalTimeConverter extends CoreConverter<LocalTime> {public LocalTimeConverter() {super(LocalTime.class);}}/*** LocalDateTime类型转换*/public static class LocalDateTimeConverter extends CoreConverter<LocalDateTime> {public LocalDateTimeConverter() {super(LocalDateTime.class);}}/*** Integer*/public static class IntegerConverter extends CoreConverter<Integer> {public IntegerConverter() {super(Integer.class);}}
}

(二)导出实现代码

package com.xiaobai.easyexcel.dynamiccolumn;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sun.istack.internal.NotNull;
import jdk.nashorn.internal.runtime.regexp.joni.ast.StringNode;
import org.apache.poi.util.IOUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;/*** @author wangtw* @ClassName DynamicColumnExport* @description: EasyExcel动态列导出* @date 2024/2/918:07*/
public class DynamicColumnExport {/**** @param d 查询数据方法参数* @param vClass 返回类型* @param getDataFun 查询数据函数式接口* @param outputStream 输出流* @param includeColumns 需要导出的列* @param writeHandlerList 自定义拦截器* @param converterList 自定义数据格式化转换器* @param <D>* @param <U>* @param <V>*/public static <D, U, V> void export(D d, Class<V> vClass,@NotNull Function<D, List<V>> getDataFun,@NotNull OutputStream outputStream,@NotNull List<String> includeColumns,@Nullable List<? extends WriteHandler> writeHandlerList,@Nullable List<? extends Converter> converterList) {/*** 1、根据需要导出的列获取每列的列名和单元格的列宽*/// 单元格宽度int columnIndex = 0;Map<Integer, Integer> columnWidthMap = new HashMap<>();//  获取表格头List<List<String>> headList = new ArrayList<>();List<Field> columnList = new ArrayList<>();Field[] declaredFields = vClass.getDeclaredFields();for (String includeColumn : includeColumns) {Optional<Field> includeColumnOptional = Arrays.stream(declaredFields).filter(f -> f.getName().equals(includeColumn)).findFirst();if (includeColumnOptional.isPresent()) {Field field = includeColumnOptional.get();field.setAccessible(true);ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);if (!ObjectUtils.isEmpty(excelProperty)) {// 列名String[] columNameArray = excelProperty.value();headList.add(Arrays.asList(columNameArray));// 可导出的列columnList.add(field);// 保存每列的宽度ColumnWidth columnWidth = field.getAnnotation(ColumnWidth.class);columnWidthMap.put(columnWidth == null ? -1 : columnWidth.value(), columnIndex++);}}}/*** 2、根据需要导出的列获取需要显示的数据*/List<List<Object>> exportDataList = new ArrayList<>();// 执行函数式接口获取需要导出的数据List<V> dataCollection = getDataFun.apply(d);for (V v : dataCollection) {// 拼装每行的数据List<Object> dataSubList = new ArrayList<>();for (Field field : columnList) {try {Object columnValue = field.get(v);dataSubList.add(Optional.ofNullable(columnValue).orElse(""));} catch (IllegalAccessException e) {e.printStackTrace();}}exportDataList.add(dataSubList);}// 表格处理策略ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream).head(headList);if (!ObjectUtils.isEmpty(writeHandlerList)) {writeHandlerList.forEach(writerBuilder::registerWriteHandler);}// 类型转换策略if (ObjectUtils.isEmpty(converterList)) {writerBuilder.registerConverter(new DataConverter.IntegerConverter());writerBuilder.registerConverter(new DataConverter.LocalDateConverter());writerBuilder.registerConverter(new DataConverter.LocalTimeConverter());writerBuilder.registerConverter(new DataConverter.LocalDateTimeConverter());} else {converterList.forEach(writerBuilder::registerConverter);}// 创建Sheet页String sheetName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();// 设置列宽writeSheet.setColumnWidthMap(columnWidthMap);// 把数据写入到Sheet页中ExcelWriter excelWriter = writerBuilder.build();excelWriter.write(exportDataList, writeSheet);// 关闭流excelWriter.finish();// 关闭输出流IOUtils.closeQuietly(outputStream);}
}

三、测试

(一)设置表头和内容策略

    /*** 设置颜色* @return*/private WriteHandler setColor() {// 头的策略WriteCellStyle headWriteCellStyle = new WriteCellStyle();// 背景设置为白色headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());WriteFont headWriteFont = new WriteFont();headWriteFont.setFontHeightInPoints((short)18);headWriteCellStyle.setWriteFont(headWriteFont);// 内容的策略WriteCellStyle contentWriteCellStyle = new WriteCellStyle();// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);WriteFont contentWriteFont = new WriteFont();// 字体大小contentWriteFont.setFontHeightInPoints((short)15);contentWriteCellStyle.setWriteFont(contentWriteFont);// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现HorizontalCellStyleStrategy horizontalCellStyleStrategy =new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);return horizontalCellStyleStrategy;}

(二)设置单元格样式策略

    public static class CellHandler implements CellWriteHandler {@Overridepublic void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {}@Overridepublic void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}@Overridepublic void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}@Overridepublic void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> list, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {CellStyle cellStyle = cell.getCellStyle();cellStyle.setBorderBottom(BorderStyle.THIN);cellStyle.setBorderTop(BorderStyle.THIN);cellStyle.setBorderLeft(BorderStyle.THIN);cellStyle.setBorderRight(BorderStyle.THIN);}}

(四)实体类

可选择某几种属性进行导出

package com.xiaobai.easyexcel.dynamiccolumn;import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;/*** @author wangtw* @ClassName DynamicData* @description: EasyExcel动态列导出* @date 2024/2/914:59*/
@AllArgsConstructor
@Builder
@Data
public class DynamicData {@ExcelProperty("id")private Integer id;@ColumnWidth(30)@ExcelProperty("姓名")private String realName;@ColumnWidth(30)@ExcelProperty("性别")private String sex;@ColumnWidth(30)@ExcelProperty("年龄")private int age;@ColumnWidth(50)@ExcelProperty("单位名称")private String orgName;@ColumnWidth(50)@ExcelProperty("部门名称")private String deptName;
}

(五)数据准备

    private List<DynamicData> getData(String condition) {List<DynamicData> dynamicDataList = new ArrayList<>();for (int i = 0; i < 100; i++) {DynamicData data = DynamicData.builder().id(i).age(random.nextInt(60)).sex("男").realName("王" + i).orgName("单位" + i).deptName("部门" + i).build();dynamicDataList.add(data);}return dynamicDataList;}

(六)测试代码(选择orgName、deptName、realName、sex进行导出)

    @Testpublic void exportTest() {File file = new File("测试.xlsx");OutputStream outputStream = null;try {outputStream = new FileOutputStream(file);} catch (FileNotFoundException e) {e.printStackTrace();}List<String> includeColumns = new ArrayList<>();includeColumns.add("orgName");includeColumns.add("deptName");includeColumns.add("realName");includeColumns.add("sex");List<WriteHandler> writeHandlers = new ArrayList<>();writeHandlers.add(setColor());writeHandlers.add(new CellHandler());DynamicColumnExport.export(null, DynamicData.class,this::getData, outputStream, includeColumns, writeHandlers, null);}

(七)效果

在这里插入图片描述

相关文章:

EasyExcel动态列导出

测试代码地址&#xff1a;https://gitee.com/wangtianwen1996/cento-practice/tree/master/src/test/java/com/xiaobai/easyexcel/dynamiccolumn 官方文档&#xff1a;https://easyexcel.opensource.alibaba.com/docs/2.x/quickstart/write 一、实现方式 1、根据需要导出的列…...

JAVA面试题11

什么是Java的访问修饰符&#xff0c;并列出它们的作用。 Java的访问修饰符包括public、private、protected和默认。它们的作用如下&#xff1a; public: 可以被任何其他类访问。 private: 只能被所在类访问&#xff0c;其他类无法访问。 protected: 可以被所在类和同一个包中的…...

工业数据采集的时间不确定性及PLC-Recorder的通道偏移功能

目录 一、缘起 二、效果展示 三、设置方法 四、小结 一、缘起 大家都知道采集软件首先要尽可能还原数据原来的状态&#xff0c;给用户提供一个可以信赖的参考。但是&#xff0c;数据采集又有很多随机因素&#xff1a;Windows是一个周期不严格的系统、以太网通讯有时间波动、…...

十五、Object 类

文章目录 Object 类6.1 public Object()6.2 toString方法6.3 hashCode和equals(Object)6.4 getClass方法6.5 clone方法6.6 finalize方法 Object 类 本文为书籍《Java编程的逻辑》1和《剑指Java&#xff1a;核心原理与应用实践》2阅读笔记 java.lang.Object类是类层次结构的根…...

计算机网络——06分组延时、丢失和吞吐量

分组延时、丢失和吞吐量 分组丢失和延时是怎样发生的 在路由器缓冲区的分组队列 分组到达链路的速率超过了链路输出的能力分组等待排到队头、被传输 延时原因&#xff1a; 当当前链路有别的分组进行传输&#xff0c;分组没有到达队首&#xff0c;就会进行排队&#xff0c;从…...

[C#] 如何调用Python脚本程序

为什么需要C#调用python&#xff1f; 有以下几个原因需要C#调用Python&#xff1a; Python拥有丰富的生态系统&#xff1a;Python有很多强大的第三方库和工具&#xff0c;可以用于数据科学、机器学习、自然语言处理等领域。通过C#调用Python&#xff0c;可以利用Python的生态系…...

AlmaLinux更换鼠标样式为Windows样式

文章目录 前言先看看条件与依赖第一步&#xff1a;测试最终效果第二步&#xff1a;使用CursorXP修改鼠标样式CurosrXP安装CursorXP使用 第三步&#xff1a;Linux端环境搭建与命令执行UbuntuFedora其他系统均失败 第四步&#xff1a;应用主题 前言 只不过是突发奇想&#xff0c…...

BUGKU-WEB 留言板

题目描述 题目无需登录后台&#xff01;需要xss平台接收flag&#xff0c; http协议需要http协议的xss平台打开场景后界面如下&#xff1a; 解题思路 看到此类的题目&#xff0c;应该和存储型xss有关&#xff0c;也就是将恶意代码保存到服务器端即然在服务器端&#xff0c;那就…...

Linux之动静态库

今天我们来讲动静态库&#xff01; 首先我们来粗粒度的划分一下动态库和静态库。 动态库就是只有一份库文件&#xff0c;所有想用该库的文件与改库文件建立链接&#xff0c;然后使用。这样可以提高代码复用率&#xff0c;避免重复拷贝产生没必要的内存消耗。 静态库&#xf…...

手机常亮屏不自动灭屏

一. 基础知识介绍 1. WakeLock&#xff08;休眠锁&#xff09; WakeLock用于保持设备的唤醒状态&#xff0c;有些情况下&#xff0c;即时用户不操作App&#xff0c;我们也需要保持屏幕处于唤醒状态&#xff0c;以保证用户体验&#xff0c;比如视频类APP和计步类APP&#xff0c;…...

JVM(1)基础篇

1 初始JVM 1.1 什么是JVM JVM 全称是 Java Virtual Machine&#xff0c;中文译名 Java虚拟机。JVM 本质上是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件。 Java源代码执行流程如下&#xff1a; 分为三个步骤&#xff1a; 编写Java源代码文件。 使用…...

相机图像质量研究(12)常见问题总结:光学结构对成像的影响--炫光

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…...

[OPEN SQL] 删除数据

DELETE语句用于删除数据库表中的数据 本次操作使用的数据库表为SCUSTOM&#xff0c;其字段内容如下所示 航班用户(SCUSTOM) 需要删除以下数据 1.删除单条数据 语法格式 DELETE <dbtab> FROM <wa>. DELETE <dbtab> FROM TABLE <itab>. DELETE FROM &…...

C语言第二十五弹---字符函数和字符串函数(上)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 目录 1、字符分类函数 2、字符转换函数 3、strlen的使用和模拟实现 4、strcpy 的模拟实现 5、strcat 的模拟实现 6、strcmp 的模拟实现 7、strncpy 函数的使用 总结…...

寒假学习记录16:Express框架(Node)

后续会补充 1.引入express 1.先下载express框架 创建一个package.json格式的文件&#xff0c;里面写入 {"dependencies": {"express": "~4.16.1" //express版本号} } 然后打开终端输入 npm i 2.引入express模块 const express require(&quo…...

机器学习中的10种非线性降维技术对比总结

降维意味着我们在不丢失太多信息的情况下减少数据集中的特征数量&#xff0c;降维算法属于无监督学习的范畴&#xff0c;用未标记的数据训练算法。 尽管降维方法种类繁多&#xff0c;但它们都可以归为两大类:线性和非线性。 线性方法将数据从高维空间线性投影到低维空间(因此…...

[ubuntu]split命令分割文件

split 命令 $ split --help Usage: split [OPTION]... [INPUT [PREFIX]] Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default size is 1000 lines, and default PREFIX is x. With no INPUT, or when INPUT is -, read standard input.Mandatory argume…...

《小强升职记:时间管理故事书》阅读笔记

目录 前言 一、你的时间都去哪儿了 1.1 你真的很忙吗 1.2 如何记录和分析时间日志 1.3 如何找到自己的价值观 二、无压工作法 2.1 传说中的“四象限法则 2.2 衣柜整理法 三、行动时遇到问题怎么办&#xff1f; 3.1 臣服与拖延 3.2 如何做到要事第一&#xff1f; 3.…...

visual studio code could not establish connection to *: XHR failed

vscode远程连接服务器时&#xff0c;输入密码&#xff0c;又重新提示输入密码&#xff0c;就这样循环了好几次&#xff0c;然后会报上述的错误。由于我是window系统&#xff0c;我用cmd&#xff0c;然后ssh */你的IP地址/*发现可以远程到服务器上&#xff0c;但是通过Vscode就不…...

JVM-面试题

一、对象 1、对象创建 类加载检查 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池定位到类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。若没有,必须先执行类加载过程。分配内存 类加载检查通过后,jvm将为新生对象分配内存,…...

19c补丁后oracle属主变化,导致不能识别磁盘组

补丁后服务器重启&#xff0c;数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后&#xff0c;存在与用户组权限相关的问题。具体表现为&#xff0c;Oracle 实例的运行用户&#xff08;oracle&#xff09;和集…...

CentOS下的分布式内存计算Spark环境部署

一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架&#xff0c;相比 MapReduce 具有以下核心优势&#xff1a; 内存计算&#xff1a;数据可常驻内存&#xff0c;迭代计算性能提升 10-100 倍&#xff08;文档段落&#xff1a;3-79…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

Neo4j 集群管理:原理、技术与最佳实践深度解析

Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

JDK 17 新特性

#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持&#xff0c;不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的&#xff…...

如何在最短时间内提升打ctf(web)的水平?

刚刚刷完2遍 bugku 的 web 题&#xff0c;前来答题。 每个人对刷题理解是不同&#xff0c;有的人是看了writeup就等于刷了&#xff0c;有的人是收藏了writeup就等于刷了&#xff0c;有的人是跟着writeup做了一遍就等于刷了&#xff0c;还有的人是独立思考做了一遍就等于刷了。…...

Linux --进程控制

本文从以下五个方面来初步认识进程控制&#xff1a; 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程&#xff0c;创建出来的进程就是子进程&#xff0c;原来的进程为父进程。…...

Java编程之桥接模式

定义 桥接模式&#xff08;Bridge Pattern&#xff09;属于结构型设计模式&#xff0c;它的核心意图是将抽象部分与实现部分分离&#xff0c;使它们可以独立地变化。这种模式通过组合关系来替代继承关系&#xff0c;从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...