java EasyExcel面向Excel文档读写逻辑示例详解

目录
  • 正文
  • 1 快速上手
    • 1.1 引入依赖
    • 1.2 导入与导出
  • 2 实现原理
    • 2.1 @RequestExcel 与 @ResponseExcel 解析器
    • 2.2 RequestMappingHandlerAdapter 后置处理器
  • 3 总结

正文

EasyExcel是一款由阿里开源的 Excel 处理工具。相较于原生的Apache POI,它可以更优雅、快速地完成 Excel 的读写功能,同时更加地节约内存。

即使 EasyExcel 已经很优雅了,但面向 Excel 文档的读写逻辑几乎千篇一律,笔者索性将这些模板化的逻辑抽离出来,该组件已经发布到 maven 中央仓库,感兴趣的朋友可以体验一下。

1 快速上手

1.1 引入依赖

<dependency>
	<groupId>io.github.dk900912</groupId>
	<artifactId>easyexcel-spring-boot-starter</artifactId>
	<version>0.0.7</version>
</dependency>

1.2 导入与导出

@RestController
@RequestMapping(path = "/easyexcel")
public class ExcelController {
    @PostMapping(path = "/v1/upload")
    public ResponseEntity<String> upload(
            @RequestExcel(sheets = {
                    @Sheet(index = 0, headClazz = User.class, headRowNumber = 1),
                    @Sheet(index = 1, headClazz = User.class, headRowNumber = 1),
                    @Sheet(index = 2, headClazz = User.class, headRowNumber = 1)
            })
            @Valid List<List<User>> users) {
        return ResponseEntity.ok("OK");
    }
    @ResponseExcel(
            name="程序猿",
            sheets = {
                    @Sheet(name = "sheet-0", headClazz = User.class),
                    @Sheet(name = "sheet-1", headClazz = User.class),
                    @Sheet(name = "sheet-2", headClazz = User.class)
            },
            suffix = ExcelTypeEnum.XLSX)
    @GetMapping(path = "/v1/export")
    public List<List<User>> export() {
        List<User> data = Lists.newArrayList();
        for (int i = 0; i < 10000; i++) {
            User user = User.builder().name("暴风赤红" + (i+1))
                    .birth(LocalDate.now()).address("江苏省苏州市科技城昆仑山路58号")
                    .build();
            data.add(user);
        }
        return ImmutableList.of(data, data, data);
    }
    @ResponseExcel(name="templates/程序猿.xlsx", scene = TEMPLATE)
    @GetMapping(path = "/v1/template")
    public void template() {}
}

2 实现原理

一切 Java 程序都是基于 Thread 的,当一个 HTTP 请求到达后,Servlet Container 会从其线程池中捞出一个线程来处理该 HTTP 请求。具体地,该 HTTP 请求首先到达 Servlet Container 的FilterChain中;然后,FilterChain 将该 HTTP 请求委派给DispatcherServlet处理,而 DispatcherServlet 恰恰就是 Spring MVC 的门户。在 Spring MVC 中,所有 HTTP 请求都由 DispatcherServlet 进行路由分发。大致流程下图所示。

DispatcherServlet 在 HandlerMapping 的帮助下可以快速匹配到最终的 Controller,由于 Controller 大多由@RequestMapping注解标注,那么RequestMappingHandlerMapping最终脱颖而出。

RequestMappingHandlerMapping 会将 HTTP 请求映射到一个HandlerExecutionChain实例中,每一个 HandlerExecutionChain 实例的内部维护了HandlerMethodList<HandlerInterceptor>

其中,HandlerMethod 实例持有一个Object类型的 bean 变量和java.lang.reflect.Method类型的 method 变量,bean 和 method 这俩成员变量组合起来最终可以确定究竟由哪一个 Controller 中的某一方法来处理当前 HTTP 请求。

此时已经知道目标方法了,那直接反射执行目标方法?是不可以的,因为通过反射来执行目标方法需要有参数才行,此外还需要对目标方法的执行结果进行加工处理。既然 HandlerMapping 没有解析请求体和处理目标执行结果的能力,只能再引入一层适配器了,它就是 HandlerAdapter。

在 Spring MVC 所提供的若干种 HandlerAdapter 中,能够适配 HandlerMethod 的只有RequestMappingHandlerAdapter

RequestMappingHandlerAdapter 实现了InitializingBean接口,用于初始化HandlerMethodArgumentResolverComposite类型的 argumentResolvers 成员变量和HandlerMethodReturnValueHandlerComposite类型的 returnValueHandlers 成员变量,Composite 后缀表明这俩成员变量均是一种复合类,argumentResolvers 持有数十种HandlerMethodArgumentResolver类型的方法参数解析器,而 returnValueHandlers 则持有数十种HandlerMethodReturnValueHandler类型的方法返回值解析器。

重点来了!首先,我们需要一个实现 HandlerMethodArgumentResolver 接口的方法参数解析器,该解析器主要用于解析@RequestExcel注解,以读取 Excel 文档;

此外,我们还需要一个实现 HandlerMethodReturnValueHandler 接口的方法返回值解析器,该解析器主要用于解析@ResponseExcel注解,以将目标方法所返回的数据写入到 Excel 文档中;

最后,将这两个自定义的解析器分别添加到 RequestMappingHandlerAdapter 中的 argumentResolvers 与 returnValueHandlers 这俩成员变量中。

在 Spring MVC 中,由RequestResponseBodyMethodProcessor负责处理@RequestBody@ResponseBody 注解。基于这一事实,笔者也没有单独设计两个解析器来分别应对 @RequestExcel 与 @ResponseExcel 注解,而是合二为一。

2.1 @RequestExcel 与 @ResponseExcel 解析器

public class RequestResponseExcelMethodProcessor implements HandlerMethodArgumentResolver,
        HandlerMethodReturnValueHandler {
    private final ResourceLoader resourceLoader;
    public RequestResponseExcelMethodProcessor(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestExcel.class);
    }
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.hasMethodAnnotation(ResponseExcel.class);
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        parameter = parameter.nestedIfOptional();
        Object data = readWithMessageConverters(webRequest, parameter);
        validateIfNecessary(data, parameter);
        return data;
    }
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // There is no need to render view
        mavContainer.setRequestHandled(true);
        writeWithMessageConverters(returnValue, returnType, webRequest);
    }
    // +----------------------------------------------------------------------------+
    // |                            private method for read                         |
    // +----------------------------------------------------------------------------+
    protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter)
            throws IOException, UnsatisfiedMethodSignatureException {
        validateArgParamOrReturnValueType(parameter);
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        Assert.state(servletRequest != null, "No HttpServletRequest");
        return readWithMessageConverters(servletRequest, parameter);
    }
    protected Object readWithMessageConverters(HttpServletRequest servletRequest, MethodParameter parameter)
            throws IOException {
        RequestExcelInfo requestExcelInfo =
                new RequestExcelInfo(parameter.getParameterAnnotation(RequestExcel.class));
        InputStream inputStream;
        if (servletRequest instanceof MultipartRequest) {
            inputStream = ((MultipartRequest) servletRequest)
                    .getMultiFileMap()
                    .values()
                    .stream()
                    .flatMap(Collection::stream)
                    .findFirst()
                    .map(multipartFile -> {
                        try {
                            return multipartFile.getInputStream();
                        } catch (IOException e) {
                            return null;
                        }
                    })
                    .get();
        } else {
            inputStream = servletRequest.getInputStream();
        }
        CollectorReadListener collectorReadListener = new CollectorReadListener();
        try (ExcelReader excelReader = EasyExcel.read(inputStream).build()) {
            List<ReadSheet> readSheetList = requestExcelInfo.getSheetInfoList()
                    .stream()
                    .map(sheetInfo -> EasyExcel.readSheet(sheetInfo.getIndex())
                            .head(sheetInfo.getHeadClazz())
                            .registerReadListener(collectorReadListener)
                            .build()
                    )
                    .collect(Collectors.toList());
            excelReader.read(readSheetList);
        }
        return collectorReadListener.groupByHeadClazz();
    }
    protected void validateIfNecessary(Object data, MethodParameter parameter) throws ExcelCellContentNotValidException {
        if (parameter.hasParameterAnnotation(Validated.class)
                || parameter.hasParameterAnnotation(Valid.class)) {
            List<Object> flattenData = ((List<List<Object>>) data).stream()
                    .flatMap(Collection::stream)
                    .collect(Collectors.toList());
            for (Object target : flattenData) {
                Set<ConstraintViolation<Object>> constraintViolationSet = ValidationUtil.validate(target);
                if (CollectionUtils.isNotEmpty(constraintViolationSet)) {
                    String errorMsg = constraintViolationSet.stream()
                            .map(ConstraintViolation::getMessage)
                            .distinct()
                            .findFirst()
                            .get();
                    throw new ExcelCellContentNotValidException(errorMsg);
                }
            }
        }
    }
    // +----------------------------------------------------------------------------+
    // |                            private method for write                        |
    // +----------------------------------------------------------------------------+
    protected void writeWithMessageConverters(Object value, MethodParameter returnType, NativeWebRequest webRequest)
            throws IOException, HttpMessageNotWritableException, UnsatisfiedMethodSignatureException {
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        ResponseExcelInfo responseExcelInfo =
                new ResponseExcelInfo(returnType.getMethodAnnotation(ResponseExcel.class));
        final String fileName = responseExcelInfo.getName();
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        if (TEMPLATE.equals(responseExcelInfo.getScene())) {
            response.setHeader("Content-disposition",
                    "attachment;filename=" + URLEncoder.encode(
                            fileName.substring(fileName.indexOf("/") + 1), StandardCharsets.UTF_8.name()));
            BufferedInputStream bufferedInputStream =
                    new BufferedInputStream(resourceLoader.getResource(CLASSPATH_URL_PREFIX + fileName).getInputStream());
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(response.getOutputStream());
            FileCopyUtils.copy(bufferedInputStream, bufferedOutputStream);
        } else {
            validateArgParamOrReturnValueType(returnType);
            response.setHeader("Content-disposition",
                    "attachment;filename=" + URLEncoder.encode(
                            fileName, StandardCharsets.UTF_8.name()) + responseExcelInfo.getSuffix().getValue());
            try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
                List<WriteSheet> writeSheetList = responseExcelInfo.getSheetInfoList()
                        .stream()
                        .map(sheetInfo -> EasyExcel.writerSheet(sheetInfo.getName())
                                .head(sheetInfo.getHeadClazz())
                                .build()
                        )
                        .collect(Collectors.toList());
                List<List<Object>> multiSheetData = (List<List<Object>>) value;
                for (int i = 0; i < writeSheetList.size(); i++) {
                    WriteSheet writeSheet = writeSheetList.get(i);
                    List<Object> singleSheetData = multiSheetData.get(i);
                    excelWriter.write(singleSheetData, writeSheet);
                }
            }
        }
    }
    // +----------------------------------------------------------------------------+
    // |                            common private method                           |
    // +----------------------------------------------------------------------------+
    private void validateArgParamOrReturnValueType(MethodParameter target) throws UnsatisfiedMethodSignatureException {
        try {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(target);
            if (!List.class.isAssignableFrom(resolvableType.resolve())) {
                throw new UnsatisfiedMethodSignatureException(
                        "@RequestExcel or @ResponseExcel Must Be Annotated With List<List<>>");
            }
            if (!List.class.isAssignableFrom(resolvableType.getGeneric(0).resolve())) {
                throw new UnsatisfiedMethodSignatureException(
                        "@RequestExcel or @ResponseExcel Must Be Annotated With List<List<>>");
            }
        } catch (Exception exception) {
            throw new UnsatisfiedMethodSignatureException(
                    "@RequestExcel or @ResponseExcel Must Be Annotated With List<List<>>");
        }
    }
}

2.2 RequestMappingHandlerAdapter 后置处理器

public class RequestMappingHandlerAdapterPostProcessor implements BeanPostProcessor,
        PriorityOrdered, ResourceLoaderAware {
    private ResourceLoader resourceLoader;
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (!supports(bean)) {
            return bean;
        }
        RequestMappingHandlerAdapter requestMappingHandlerAdapter = (RequestMappingHandlerAdapter) bean;
        List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        List<HandlerMethodReturnValueHandler> returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        Assert.notEmpty(argumentResolvers,
                "RequestMappingHandlerAdapter's argument resolver is empty, this is illegal state");
        Assert.notEmpty(returnValueHandlers,
                "RequestMappingHandlerAdapter's return-value handler is empty, this is illegal state");
        List<HandlerMethodArgumentResolver> copyArgumentResolvers = new ArrayList<>(argumentResolvers);
        RequestResponseExcelMethodProcessor argumentResolver4RequestExcel = new RequestResponseExcelMethodProcessor(null);
        copyArgumentResolvers.add(0, argumentResolver4RequestExcel);
        requestMappingHandlerAdapter.setArgumentResolvers(Collections.unmodifiableList(copyArgumentResolvers));
        List<HandlerMethodReturnValueHandler> copyReturnValueHandlers = new ArrayList<>(returnValueHandlers);
        RequestResponseExcelMethodProcessor returnValueHandler4ResponseExcel = new RequestResponseExcelMethodProcessor(resourceLoader);
        copyReturnValueHandlers.add(0, returnValueHandler4ResponseExcel);
        requestMappingHandlerAdapter.setReturnValueHandlers(Collections.unmodifiableList(copyReturnValueHandlers));
        return requestMappingHandlerAdapter;
    }
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
    private boolean supports(Object bean) {
        return bean instanceof RequestMappingHandlerAdapter;
    }
}

3 总结

目前该版本仅支持针对单个 Excel 文档的导入与导出(多Sheet是支持的哈),所以由 @RequestExcel 注解修饰的方法参数必须是一个List<List<>>类型,而由 @RequestExcel 注解修饰的方法返回类型也必须是一个List<List<>>类型,否则将抛出UnsatisfiedMethodSignatureException类型的自定义异常。

坦白来说,该组件的设计初衷只是为了帮助大家从公式化、模板化的Excel 读写逻辑中解放出来,从而专注于核心业务逻辑的开发,并不是为了增强 EasyExcel,后续也不会朝着这一方向演进。

参考 github.com/dk900912/ea…

以上就是java EasyExcel面向Excel文档读写逻辑示例详解的详细内容,更多关于java EasyExcel读写逻辑的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java+EasyExcel实现文件的导入导出

    目录 引言 效果图 项目结构 核心源码 核心实体类 核心监听器类 EasyExcel导入文件 EasyExcel导出文件 引言 项目中需要Excel文件的导入与导出Excel并下载,例如,导入员工信息,导出员工信息,手动输入比较繁琐,所以本篇博文教大家如何在Java中导入Excel文件与导出Excel文件 技术栈 Excel工具:EasyExcel 选用框架:Spring.Spring MVC.MyBatis(SSM) 项目构建管理工具:Maven 需求: 1.要求利用excel工具实现员工信息

  • Java中Easyexcel 实现批量插入图片功能

    目录 1 Maven依赖 2 PictureModel 3CustomPictureHandler 4 调试代码 5 调试结果 注: 注: 1 Maven依赖 <!--hutool工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.1</version> </de

  • Java使用EasyExcel动态添加自增序号列

    目录 前言 实现 思路 其它 总结 前言 本文将介绍如何通过使用EasyExcel自定义拦截器实现在最终的Excel文件中新增一列自增的序号列,最终的效果如下: 此外,本文所使用的完整代码示例已上传到GitHub. 实现 本文主要是通过自定义一个继承AbstractRowWriteHandler的拦截器来实现在最终导出的结果中新增序号列,通过修改源码中保存头部标题的Map内容来给自己添加的序号列留出位置,先展示最终的代码: /** * 自定义 excel 行处理器, 增加序号列 * * @aut

  • Java利用EasyExcel实现合并单元格

    目录 pom版本 1.自定义合并单元格 1.1 不合并单元格 1.2 合并单元格 1.3 写多个sheet 1.4 WriteTable pom版本 <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.7</version> </dependency> 1.自定义合并单元格 在某些

  • java使用EasyExcel导入导出excel

    一.准备工作 1.导包 <!-- poi 相关--> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId

  • Java应用EasyExcel工具类

    一.前言 关于EasyExcel,它对poi做了进一步的封装,使得整个编写流程更加的面向对象.好处嘛,我认为流程上更加清晰即易懂.可读性更好,坏处的话,则是操作上没有原生的方式那么灵活. 二.导入 StudentVo为实体类, 注意实体中的各个属性要和excel按顺序一 一对应,建议都用String类型,真正插入时,才去做转换 ImportExcelListener 类为真正处理数据的类 CommonService 只是一个Spring的service bean,用来执行curd操作 priva

  • java EasyExcel面向Excel文档读写逻辑示例详解

    目录 正文 1 快速上手 1.1 引入依赖 1.2 导入与导出 2 实现原理 2.1 @RequestExcel 与 @ResponseExcel 解析器 2.2 RequestMappingHandlerAdapter 后置处理器 3 总结 正文 EasyExcel是一款由阿里开源的 Excel 处理工具.相较于原生的Apache POI,它可以更优雅.快速地完成 Excel 的读写功能,同时更加地节约内存. 即使 EasyExcel 已经很优雅了,但面向 Excel 文档的读写逻辑几乎千篇一

  • Flask实现swagger在线文档与接口测试流程详解

    目录 1.什么是restful 2.swagger/openAPI能做什么 3.python如何实现swagger 4.flasgger的使用案例 5.完整代码 阅读对象:知道什么是restful,有了解swagger或者openAPI更佳. 1.什么是restful Representional State Transfer(REST):表征状态转移.是一种一种基于HTTP协议的架构.采用Web 服务使用标准的 HTTP 方法 (GET/PUT/POST/DELETE) 将所有 Web 系统的

  • 对tensorflow中cifar-10文档的Read操作详解

    前言 在tensorflow的官方文档中得卷积神经网络一章,有一个使用cifar-10图片数据集的实验,搭建卷积神经网络倒不难,但是那个cifar10_input文件着实让我费了一番心思.配合着官方文档也算看的七七八八,但是中间还是有一些不太明白,不明白的mark一下,这次记下一些已经明白的. 研究 cifar10_input.py文件的read操作,主要的就是下面的代码: if not eval_data: filenames = [os.path.join(data_dir, 'data_b

  • go实现Redis读写分离示例详解

    目录 我们为什么需要了解RESP协议? 什么是RESP协议 RESP协议规范 如何使用该协议请求Redis 使用go编写Redis中间件实现读写分离 总结 我们为什么需要了解RESP协议? 本篇文章目的为探究RESP协议,而非编写读写中间件,这点要清楚. 关于这个问题,我想通过一个实例来解释,我们编写Redis中间件,为什么需要了解RESP协议. 以上代码是编写了一个非常简单的TCP服务器,我们监听8888端口,尝试使用redis-cli -p 8888连接服务器后,而后查看打印出来的应用层报文

  • java中常见的6种线程池示例详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如下 线程池名称 描述 FixedThreadPool 核心线程数与最大线程数相同 SingleThreadExecutor 一个线程的线程池 CachedThreadPool 核心线程为0,最大线程数为Integer. MAX_VALUE ScheduledThreadPool 指定核心线程数的定时

  • java zxing合成复杂二维码图片示例详解

    目录 说明: 整体思路: 图片合成四部曲 踩过的坑 说明: 最近接到需要将二维码合成复杂图片的需求,要求给二维码上下或者左侧添加相关文字描述,技术没有难点,整理本文主要记录思路和踩过的坑. 整体思路: 引入zxing成熟的二维码生成接口,生成标准二维码文件,通过java图形图像处理API为二维码添加相关文字描述,根据需要,可以为合成后的图片添加相关背景.示例如下图所示: 1.先拿点位图来说,生成二维码图片核心代码如下 /** * 定义二维码的参数 */ HashMap<EncodeHintTyp

  • java接口性能从20s优化到500ms示例详解

    目录 前言 1. 案发现场 2. 现状 3. 第一次优化 4. 第二次优化 5. 第三次优化 5.1 前端做分页 5.2 分批调用接口 前言 接口性能问题,对于从事后端开发的同学来说,是一个绕不开的话题.想要优化一个接口的性能,需要从多个方面着手. 其实,我之前也写过一篇接口性能优化相关的文章<java接口性能优化小技巧>,发表之后在全网广受好评,感兴趣的小伙们可以仔细看看. 本文将会接着接口性能优化这个话题,从实战的角度出发,聊聊我是如何优化一个慢查询接口的. 上周我优化了一下线上的批量评分

  • java中Servlet监听器的工作原理及示例详解

    监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行. 监听器原理 监听原理 1.存在事件源 2.提供监听器 3.为事件源注册监听器 4.操作事件源,产生事件对象,将事件对象传递给监听器,并且执行监听器相应监听方法 监听器典型案例:监听window窗口的事件监听器 例如:swing开发首先制造Frame**窗体**,窗体本身也是一个显示空间,对窗体提供监听器,监听窗体方法调用或者属性改变:

  • java面向对象设计原则之里氏替换原则示例详解

    目录 概念 实现 拓展 概念 里氏替换原则是任何基类出现的地方,子类一定可以替换它:是建立在基于抽象.多态.继承的基础复用的基石,该原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余. 实现 里氏替换原则要求我们在编码时使用基类或接口去定义对象变量,使用时可以由具体实现对象进行赋值,实现变化的多样性,完成代码对修改的封闭,扩展的开放.如:商城商品结算中,定义结算接口Istrategy,该接口有三个具体实现类,分别为PromotionalStrategy (满减活动,两

  • java面向对象设计原则之接口隔离原则示例详解

    目录 概念 实现 拓展 概念 小接口原则,即每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分.如下图所示定义了一个接口,包含了5个方法,实现类A用到了3个方法.实现类B用到了3个方法,类图如下: 类A没有方法4.方法5,却要实现它:类B没有方法2.方法3,但还是要实现这两个方法,不符合接口隔离原则.改造为将其拆分为三个接口,实现方式改为下图所示,符合接口隔离原则: 实现 面向对象机制中一个类可以实现多个接口,通过多重继承分离,通过接口多继承(实现)来实现客户的需求,代码更加清

随机推荐