FOSS List梳理,MySQL (mysql-connector-j) 是GPL

一、FOSS List 梳理
别说多痛苦了,反正就是非常繁琐的工作

那什么是FOSS 以及 FOSS List?
FOSS(Free and Open-Source Software)是指自由和开放源代码软件。
FOSS List是一个用于收集和整理开源软件的Excel
FOSS List列出了各种开源软件的名称、版本、授权协议等信息。

二、MySQL开源组件又是什么呢?
MySQL是一个流行的关系型数据库管理系统(RDBMS),由瑞典公司MySQL AB开发,后来被Sun Microsystems收购。MySQL的源代码是使用C和C++编写的,并通过GPL许可证发布。这意味着任何人都可以获取并修改MySQL的源代码,但同时必须以相同的许可证发布修改后的版本。

MySQL(mysql-connector-j)是遵循GPL(GNU General Public License)许可证的开源软件。
MySQL (mysql-connector-j) 是使用 GPL 许可证进行发布的。这意味着,如果你使用 MySQL (mysql-connector-j),你需要遵守 GPL 许可证的条款和条件,包括发布你的软件的源代码。如果你不想遵守 GPL 许可证,那么你可能需要寻找其他数据库选项。

MySQL的开源协议基于GPL或Commercial License,任何公司都可以免费使用,不允许修改后和衍生的代码做为闭源的商业软件发布和销售,MySQL的版权在甲骨文手中,甲骨文可以推出其商业闭源版本

mysql-connector-j是MySQL的官方Java驱动程序,用于在Java应用程序中使用MySQL数据库。它也是使用GPL许可证发布的,因此遵循相同的规则和要求。这意味着任何接受mysql-connector-j许可证的Java应用程序也必须以自由软件的形式发布。

需要注意的是,虽然MySQL和mysql-connector-j是遵循GPL许可证的开源软件,但这并不意味着它们必须与其他软件一起使用或链接。因此,如果您使用MySQL或mysql-connector-j作为您自己的应用程序的一部分,您可以选择使用不同的许可证来发布您的应用程序,只要不违反GPL许可证的规定即可。

三、 GPL 许可证有什么特点?
GNU通用公共许可证简称为GPL,公司的AD+DD文档中对 Licenses(授权)章节重点写了一句,GNU公共许可证不得用于公司内的软件开发。

GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。

GPL的“传染性”在于:只要在一个软件中使用(”使用”指类库引用,修改后的代码或者衍生代码)GPL 协议的产品,则该软件产品必须也采用GPL协议,既必须也是开源和免费。我们所熟悉的Linux就是采用了GPL。

GPL是一种广泛使用的开源许可证,适用于许多自由软件项目。根据GPL许可证,任何接受该许可证的软件必须以自由软件的形式发布,这意味着任何人都可以自由地获取、修改和分发该软件。

本文出自:https://blog.csdn.net/wstever/article/details/133268860

Java 关键字:synchronized详解

Java中的synchronized关键字用于在多线程环境下确保数据同步。它可以用来修饰方法和代码块

当一个线程访问一个对象的synchronized方法或代码块时,其他线程将无法访问该对象的其他synchronized方法或代码块。这样可以确保在同一时间只有一个线程能够执行该代码块或方法,避免了多线程环境下的数据不一致问题,例如:

public class SynchronizedExample {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}

在上面的代码中,increment()方法是一个synchronized方法。当多个线程访问这个方法时,只有一个线程能够执行该方法的代码,其他线程将被阻塞。

synchronized关键字也可以用来修饰代码块,如:

public void increment() {
    synchronized(this) {
        count++;
    }
}

在上面的代码中,synchronized关键字修饰的是一个代码块,并且锁对象是当前对象(this)

注意:synchronized关键字会导致线程上下文切换和资源竞争,所以在使用时要注意性能问题

 

源码解析

底层实现是通过 Java 虚拟机(JVM)的对象头和监视器锁机制实现的

 

具体来说,当一个线程访问一个对象的 synchronized 方法或代码块时,它会试图获取该对象的监视器锁。如果该锁未被其他线程占用,该线程将获得该锁并执行代码;如果该锁被其他线程占用,该线程将进入阻塞状态,等待获取该锁

 

synchronized 是Java中用于实现同步的关键字,它在底层通过监视器锁(Monitor)来实现。下面是synchronized的源码解析:

 

在Java中,每个对象都有一个与之关联的监视器锁,也称为内置锁或对象锁。当线程进入一个synchronized方法或代码块时,它会尝试获取该对象的监视器锁。如果锁没有被其他线程占用,则该线程获得锁并开始执行代码;如果锁已经被其他线程占用,则该线程将被阻塞,直到锁被释放。

 

在Java虚拟机中,每个对象头中都包含一部分用于实现synchronized的相关信息。这些信息包括:

 

mark word:用于存储对象的标记信息,包括锁的状态。

Klass pointer:指向对象的类元数据,包括synchronized的相关信息。

monitor:与对象关联的监视器,它记录了当前占用锁的线程、等待锁的线程队列等。

当一个线程尝试获取一个对象的锁时,虚拟机会检查对象头中的标记信息。如果对象的锁状态为无锁状态,即未被其他线程占用,则该线程可以获取锁,并将标记信息设置为锁定状态。如果对象的锁状态为已锁定,并且当前线程是锁的所有者,则该线程可以继续执行代码。如果对象的锁状态为已锁定,并且当前线程不是锁的所有者,则该线程将被放入等待队列中,进入阻塞状态。

 

当持有锁的线程执行完synchronized方法或代码块后,它会释放锁,即将对象头中的锁状态置为无锁状态,并唤醒等待队列中的一个线程,使其获取锁并继续执行。

 

需要注意的是,synchronized关键字可以修饰方法和代码块。在方法上修饰的synchronized表示对整个方法进行同步,而在代码块上修饰的synchronized表示对该代码块进行同步,使用的锁对象通常是方法所属对象或指定的对象。

 

总结起来,通过监视器锁的机制,Java的synchronized能够保证同一时刻只有一个线程访问同步代码块或方法,避免了多线程的数据竞争和并发问题。

 

这里给出一份简化的 synchronized 关键字的源码:

public void synchronized method() {
    // 加锁
    Monitor.enter(this);
    try {
        // 同步代码块
    } finally {
        // 释放锁
        Monitor.exit(this);
    }
}

在这份代码中,方法通过调用 Monitor.enter 方法获取当前对象的监视器锁,并在 finally 块中调用 Monitor.exit 方法释放该锁。因此,在 synchronized 方法内部的代码可以保证在任意时刻只有一个线程可以访问

 

常见面试题

synchronized 方法和 synchronized 块的区别是什么?

作用范围:synchronized 方法将整个方法体作为同步区块,而 synchronized 块可以将任意代码块作为同步区块

锁的对象:synchronized 方法锁定的是整个对象,而 synchronized 块锁定的是在括号内指定的对象

可控性:synchronized 方法的同步粒度比较大,不够灵活;而 synchronized 块可以更灵活地控制同步代码块的大小

综上所述,在确定同步粒度时,通常使用 synchronized 块比使用 synchronized 方法更灵活,但是如果整个方法都需要同步,使用 synchronized 方法会更加简单易懂

什么情况下可以使用 synchronized 关键字?

synchronized 关键字可以用于在多线程环境下保证方法或代码块的原子性。具体来说,如果一个线程正在执行同步方法或代码块,则其他线程将无法访问该方法或代码块

常见情况包括:

当多个线程访问共享资源时,可以使用 synchronized 关键字保证线程的安全

在访问共享变量时,需要对其进行同步控制

在线程通信中,可以使用 synchronized 关键字保证线程之间的同步通信

synchronized 关键字的性能开销如何?

synchronized 关键字的使用会带来一些性能开销,因为它需要在多个线程之间进行同步。当线程访问同步代码块时,它必须获得锁,这会增加额外的开销。如果同步代码块执行时间过长,其他线程将一直等待,进而降低程序的性能。

因此,应该尽量避免在高并发情况下使用 synchronized,或者使用其他的并发控制机制,如 java.util.concurrent 包中的锁和原子操作类等。

synchronized 关键字如何实现可重入?

“可重入” 指的是同一线程可以多次获取同一个锁。例如,当线程 A 进入一个同步块时,如果它再次试图进入该块,则可以再次获取锁,而不会发生死锁

在 Java 中,synchronized 关键字可以实现可重入,原因如下:

synchronized 关键字使用对象监视器锁来实现同步。

对象监视器锁是基于线程的,并且每个线程有一个独立的计数器,用于跟踪它在当前对象上获取的锁的数量。

当线程试图获取锁时,如果它已经拥有该锁,则计数器将递增。

当线程退出同步块时,计数器将递减。

只有当计数器为零时,该线程才会释放锁。

因此,如果一个线程在同一对象上多次进入同步块,它将多次获得该锁,并在退出该块时多次释放该锁。因此,synchronized 关键字是可重入的。

synchronized 关键字与 lock 机制的比较?

synchronized 关键字和 Lock 机制都是用来保证线程同步的方法。但是它们有一些明显的差异:

灵活性:Lock 机制比 synchronized 关键字更灵活,因为它提供了更多的锁定操作,例如可以实现公平锁和非公平锁,还可以实现读写锁。

可中断性:Lock 机制可以中断一个线程的等待,而 synchronized 关键字不能。

可重入性:synchronized 关键字是自动可重入的,而 Lock 机制必须手动实现。

性能:如果比较的是相同的锁定操作,synchronized 关键字通常比 Lock 机制更快,因为它是内置的。

总体而言,在简单的同步情况下,synchronized 关键字更方便,但是在需要更多灵活性的情况下,Lock 机制可能是一个更好的选择。

本文出自:https://blog.csdn.net/qq_54796785/article/details/133913211

提高代码质量的秘诀:类、方法、字段和包注释

JDK提供了一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。如果在源代码中添加以特殊定界符/**开始的注释,那么你就可生成一个看上去具有专业水准的文档。Java文档注册可以提高代码的可读性和维护性。其中的关键是使用合适的注释,Java的注释有许多种类:

 

🍢 一、注释的插入

在Java中添加注释非常简单。只需要在代码前面加上两个斜线“//”,就可以在该行之后添加单行注释:

 

// 这是单行注释

你还可以将任何文本放在一个多行注释块内:

/*
 
● 多行注释。
 
●  在块注释中,可以分解成多段注释。

 */

🍣 二、类注释

/**
 
● 这是类的说明 */

其中,星号(*)后的内容是用于为Java文档工具生成类库文档所需的标记。它包含了类的一般描述和一些相关信息。这样的类注释使得您的代码复用更加容易。

🍤 三、方法注释

/**
 
这是方法的说明
@param p1 参数说明
@param p2 参数说明
@return 返回值说明
@exception 异常说明 */

这种注释中带有参数说明、返回值说明和异常说明,旨在帮助开发人员理解当前方法的目的和如何使用它。

🍥 四、字段注释

字段注释应在字段的定义之前进行,在格式上与变量声明类似:

/**
 
对于该字段的描述 */ 
private String fieldName;

这样可以对自身或者其他开发人员解释一个字段存在的意义,非常有利于代码理解和后期维护。

🥮 五、通用注释

标记 @since text 会建立一个 “since”(始于)条目。text(文本)可以是对引入这个特性的版本的描述。例如:@since 1.7.1。

 

@author name

这个标记将建立一个“author”(作者)条目,可以由多个@author标记,每个@author标记对应一个作者。并不是非得使用这个标记,你的版本控制系统能够更好地跟踪作者。

 

@version text

这个标记将建立一个“version”(版本)条目。这里的text可以是对当前版本的任何描述。

 

通过@see和@link标记,可以使用超链接,链接到javadoc文档的相关部分或外部文档。

🍡 六、包注释

要想产生包注释,就需要在每个包目录中添加一个单独的文件。可以由如下两个选择:

 

1、提供一个名为package-info.java的Java文件。这个文件必须包含一个初始的Javadoc注释,以/**和*/界定,后面是一个package语句。它不能包含更多的代码或注释。

 

2、提供一个名为package.html的HTML文件,抽取标记<body>…<body>之间的所有文本

/**
 
package-info 文件提供关于当前包的全局说明,
可以简要描述该包中提供的类、函数和语言结构以及为何这些元素会彼此相关联。 */

 

本文出自:https://blog.csdn.net/m0_61961937/article/details/131032213

Java 图片验证码需求分析

图片验证码

需求分析

连续因输错密码而登录失败时,记录其连续输错密码的累加次数;若在次数小于5时,用户输入正确的密码并成功登录,则次数被清零

连续5次因输错密码而登录失败后,系统弹框提示【您已连续5次输入错误的密码,暂时不允许登录,请10分钟后再次尝试登录】;点击提示框中的【确定】按钮,提示框被关闭

10分钟内再次尝试登录,则系统弹框提示【您已连续5次输入错误的密码,暂时不允许登录,7分43秒后可再次尝试登录】;点击提示框中的【确定】按钮,提示框被关闭;注:提示框中的剩余时间动态倒数至0分0秒

10分钟后,用户可再次尝试登录;此时,若用户在输错密码次数小于5次时成功登录,则其连续输错密码的次数、曾被锁定1次的信息被清空归零;反之,若用户再次连续5次输错密码,则系统弹框提示【您已连续10次输入错误的密码,账号已被锁定、不允许登录,请联系管理员解锁】;点击提示框中的【确定】按钮,提示框被关闭。此后,用户每次用该账号尝试登录时,均弹出此提示框。此时,在运营端,该用户详情页面中的【登录状态】已被自动切换为【锁定】。用户须主动联系莫族密运营人员,运营人员确认用户没有被盗号、遭遇网络攻击等风险后,主动将其【登录状态】置为【解锁】;此时,用户连续输错密码的次数、曾被锁定2次的信息被清空归零

用户登录时,须输入正确的【验证码】

若用户看不清,则可点击【看不清?换一张】字样,也可直接点击验证码部件,点击后自动刷新验证码

点击【登录】按钮后,【用户名】、【密码】、【验证码】这3项但凡有1项校验不通过,则登录失败,【用户名】、【密码】、【验证码】框中已录入的内容被清空,验证码自动刷新

点击【登录】按钮后,若【用户名】、【密码】校验通过,唯独【验证码】校验不通过,则登录失败的系统提示内容为【验证码错误,请重新录入验证码】。同时验证码自动刷新。

【验证码】的有效时间为60秒,超过之后则失效,但不自动刷新。失效之后若录入正确的【用户名】【密码】同时录入页面上已失效的【验证码】,则登录失败,且登录失败的系统提示内容为【验证码错误,请重新录入验证码】,同时验证码自动刷新

实施

验证码接口 | 请求头方式传递

依赖导入

<!-- 添加图形验证码依赖 -->
       <dependency>
           <groupId>cn.hutool</groupId>
           <artifactId>hutool-captcha</artifactId>
           <version>5.8.5</version>
       </dependency>

图片验证码接口编写

/**
     * 生成验证码图片
     * @return
     */
    @ApiOperation("获取图形验证码")
    @GetMapping("/identifyImage")
    public Result<String> identifyImage(HttpServletResponse response,
                                        @ApiParam(value = "图形验证码id,无值:生成验证码,有值:刷新验证码")
                                        @RequestParam(name = "codeId", required = false) String codeId) throws IOException {
        // 创建验证码,设置宽、高、长度、干扰线数量
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 90, 4, 100);
        // 获取验证码字符串,赋值code
        String code = lineCaptcha.getCode();
        if (codeId == null) {
            // IdWorker.getId():IdWorker工具类生成唯一ID,并转换成String类型
            codeId = String.valueOf(IdWorker.getId());
            // 将codeId、code.toUpperCase()、过期时间60秒:存储入Redis中
            // code.toUpperCase():code装换成大写形式存储
            redisOps.set(codeId,code.toUpperCase(),60);
        } else {
            redisOps.set(codeId,code.toUpperCase(),60);
        }
        // 将图片验证码codeId设置请求头中
        response.setHeader("codeId", codeId);
        // 获取向客户端发送响应数据的输出流
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            // 验证码图片数据写入到输出流
            lineCaptcha.write(outputStream);
        } catch (Exception e) {
            throw new AuthException("图形验证码输出错误");
        }
        return Result.succ(codeId);
    }

Postman调用测试

http://localhost:9036/api/identifyImage

验证码接口 | base64方式传递

/**
     * 生成验证码图片
     * @return
     */
    @ApiOperation("获取图形验证码")
    @GetMapping("/identifyImage")
    public Result<IdentifyImageResp> identifyImage(HttpServletResponse response,
                                                   @ApiParam(value = "图形验证码id,无值:生成验证码,有值:刷新验证码")
                                        @RequestParam(name = "codeId", required = false) String codeId) throws IOException {
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 90, 4, 100);
        String code = lineCaptcha.getCode();
        if (codeId == null) {
            codeId = String.valueOf(IdWorker.getId());
            redisOps.set(codeId,code.toUpperCase(),60);
        } else {
            redisOps.set(codeId,code.toUpperCase(),60);
        }
        IdentifyImageResp identifyImageResp = new IdentifyImageResp(codeId, lineCaptcha.getImageBase64Data());
        return Result.succ(identifyImageResp);
    }

登录接口

登录接口编写

@PostMapping("login")
@ApiOperation("用户登录")
public Result login(@Validated @RequestBody LoginRequest request) {
    // request.getCodeId():请求体中获取codeId
    // redisOps.get(request.getCodeId():codeId为键,获取redis中对应的值
    String codeId = (String) redisOps.get(request.getCodeId());
    if (codeId.isEmpty()){
        throw new AuthException("验证码已过期请刷新重试");
    }
    AuthContext login = authService.login(request);
    
    // 登录成功后,通过 login.getMerchant() 获取到登录的用户对象,跟新登录信息
    Merchant merchant = login.getMerchant();
    merchant.setLastLoginAt(merchant.getLoginAt());
    merchant.setLoginAt(new Date());
    merchant.setLastLoginIp(merchant.getLoginIp());
    merchant.setLoginIp(CommonTools.getIp(httpServletRequest));
    merchantRepo.updateById(merchant);
    login.setMerchant(merchant);
    // JsonMapper.objectToJson(login):将login对象转换成 JSON 格式的字符串
    log.info("LOGIN - > {}", JsonMapper.objectToJson(login));
    return Result.succ("登录成功");
LoginRequest.java:请求体字段
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {

    @NotEmpty
    @ApiModelProperty("登录名")
    private String username;
    @NotEmpty
    @ApiModelProperty("密码,md5加密全小写")
    private String password;
    @ApiModelProperty("验证码")
    private String code;
    @ApiModelProperty("验证码Id")
    private String codeId;

}
AuthService.java
    public AuthContext login(LoginRequest login) {
        // 登录验证和处理
        if (StringUtils.isBlank(login.getUsername())) {
            throw new AuthException("用户名不能为空");
        }
        if (StringUtils.isBlank(login.getPassword())) {
            throw new AuthException("密码不能为空");
        }
        // 缓存清空,登出操作
        logout();
        Merchant merchant = findMerchantByLoginEmail(login.getUsername());
        if (merchant == null) {
            authError("账户不存在,或状态不正确");
        } else if (merchant.getIsLocked()) {
            authError("账户已停用");
        }
        // 从redis获取login.getUsername()+"lock-time")的键对应的值
        if (redisOps.get(login.getUsername()+"lock-time") != null){
            // redisOps.getExpire:获取 Redis 中指定键的过期时间
            long expire = redisOps.getExpire(login.getUsername() + "lock-time");
            // 转换为分钟
            int minutes = (int) (expire / 60);
            // 转换为秒钟
            int seconds = (int) (expire % 60);
            authError("您已连续5次输入错误的密码,暂时不允许登录,"+minutes+"分"+seconds+"秒后可再次尝试登录");

        }
        System.out.println(merchant.getLoginPassword());
        System.out.println(SecretUtils.encrypt(login.getPassword()));
        Integer errorNum = (Integer) redisOps.get(login.getUsername());
        if (!merchant.getLoginPassword().equals(SecretUtils.encrypt(login.getPassword()))) {
            //密码错误次数为null时创建键值对
            if (errorNum == null){
                redisOps.set(login.getUsername(),1);
            }else if  ((errorNum > 0 && errorNum < 4) || (errorNum > 5 && errorNum < 10)){
                //密码错误次数为0-4、5-10时incr
                redisOps.incr(login.getUsername(),1);
            }else if (errorNum+1==5){
                //密码错误次数为5时锁定10分钟
                redisOps.set(login.getUsername()+"lock-time","lock",600);
                authError("您已连续5次输入错误的密码,暂时不允许登录,请10分钟后再次尝试登录");
            }else {
                //密码错误次数为10时锁定
                merchant.setIsLocked(true);
                merchantRepo.updateById(merchant);
                authError("您已连续10次输入错误的密码,账号已被锁定、不允许登录,请联系管理员解锁");
            }
            authError("密码不正确");
        }
        String code= (String) redisOps.get(login.getCodeId());
        if (code == null || login.getCode()==null || !code.equals(login.getCode().toUpperCase())){
            authError("请输入正确的验证码");
        }
//        merchant.setLoginPassword("*");
        String token = Sha.sha256(UUID.randomUUID().toString());
        AuthContext authContext = new AuthContext(token, merchant, null);
        redisOps.set(token, JsonMapper.objectToJson(authContext), authProp.getExpiresSeconds());
        CookieUtils.setCookie(response, "/", authProp.getTokenHeader(), token, authProp.getExpiresSeconds());
        //登陆完成删除账号错误次数
        if (errorNum!=null)
            redisOps.delete(login.getUsername());
        return authContext;
    }

    public String logout() {
        String cookie = CookieUtils.getCookie(request, authProp.getTokenHeader());
        String token = StringUtils.isNotBlank(cookie) ? cookie : request.getHeader(authProp.getTokenHeader());
        if (StringUtils.isNotBlank(token)) {
            redisOps.delete(token);
        }
        return "登出成功";
    }

    private void authError(String errorMsg) {
        throw new AuthException(errorMsg);
    }

 

 

 

本文出自:https://blog.csdn.net/weixin_62765017/article/details/131963978

纯后台生成Echarts图片,并将图片生成到PDF文档

一、如何后台生成Echarts图片?

1.PhantomJS

后台生成Echarts图,需要使用到PhantomJS:一个自带JavaScript API的无头WebKit脚本,简单理解就是:它能干浏览器能干的几乎所有事情,能解析js、能渲染等等,但是没有页面让你看。

 

2.PhantomJS的下载

https://phantomjs.org/api/fs/

 

http://wenku.kuryun.com/docs/phantomjs/index.html

 

也可以直接用我下载的文件:我的文件链接  提取码:es4e

 

打开我的文件:根据你使用的操作系统,选择一个进行下载并解压。

打开我的文件,除phantomjs文件外,可见还有一个文件夹echarts-convert,有2个文件:

 

1.echarts:里面是echarts和jquery,可以用你们自己本地或项目中的。

 

2.echarts-converts.js:是自己写的js文件,用来渲染生成echarts图片,可根据自己需求改写。

注意:若使用自己本地的echarts和jquery文件,echarts-converts.js里面的文件路径需要改写,指向你的文件所在的位置。

3.用phantomjs调用echarts-converts.js生成图片

以下以windows环境下举例:

 

1.从echarts官网随便选择一个图,将option复制本地某个文件中(例:G:\test\testOption.txt)

 

2.手动创建一个空的png文件(例:G:\test\111.png)

3.cmd调用phantomjs进程,让它去解析echarts-converts.js,并传入一些参数(如图):

 

G:\test\phantomjs-2.1.1-windows\bin\phantomjs.exe G:\test\echarts-convert\echarts-convert.js -txtPath G:\test\testOption.txt -picTmpPath G:\test\111.png -picPath G:\test\222.png

4.执行完成,完成后,111.png图片有内容,且生成了一张222.png。

二、Java如何将Echarts图生成到PDF

前面已经可以通过手动调用的方式生成Echarts图片。

 

接下来需要做的:

 

1、将手动生成图片的过程通过代码实现。

 

2、将生成的图片生成出PDF

 

3、下载PDF

 

1.生成PDF依赖

<dependency>

<groupId>com.itextpdf</groupId>

<artifactId>itextpdf</artifactId>

<version>5.5.13.3</version>

</dependency>

2.Java代码测试例子:

public static void main(String[] args)

{

try

{

// 测试同时生成两张图

// 注意:要先手动创建两个空文件G:\\test\\1.png和G:\\test\\2.png;要提前将echarts的option数据写到G:\\test\\testOption.txt和G:\\test\\testOption2.txt中

doExecPhantomJS_deleteFileAfterException(“G:\\test\\testOption.txt,G:\\test\\testOption2.txt”, “G:\\test\\1.png,G:\\test\\2.png”, “G:\\test\\111.png,G:\\test\\222.png”);

}

catch (Exception e)

{

e.printStackTrace();

}

 

Paragraph ph1 = ReportFormUtil.createImageParagraphByPath(“1、段落标题111”, “G:\\test\\111.png”, 35, 35,”这是一段前缀描述信息111″, “这是一段后缀描述信息111”);

Paragraph ph2 = ReportFormUtil.createImageParagraphByPath(“2、段落标题222”, “G:\\test\\222.png”, 35, 35, “这是一段前缀描述信息222”, “这是一段后缀描述信息222”);

 

List<Paragraph> phs = new ArrayList<Paragraph>();

phs.add(ph1);

phs.add(ph2);

 

ReportFormUtil.createPDFDocumentToDisk(“封面名称”, “小标题”, “”, phs, “G:\\test\\document.pdf”);

}

 

Tips:

 

一、ReportFormUtil文件放在文章结尾。

 

二、关于如何自动生成默认样式的option.txt文件,不过多赘述:

 

我的主要做法是:option配置可以看成一个json格式的字符串,然后:

 

1、提供可配置的默认的option样式;

 

2、将option中的数据部分(想自定义的部分)用符号占位;

 

3、在需要生成图片时,结合业务计算出自定义数据并转成json,替换掉占位符填到option字符串中生成完整的带数据的option字符串。

 

4、将3中生成的完整option字符串写到指定临时txt文件中(流程结束后删除临时文件)

 

3.测试结果

测试结果如图所示:成功生成pdf文件,且生成了两个图片段落

三、下载生成的PDF

@Override
    public void download(HttpServletResponse response, HttpServletRequest request) throws BusinessException
    {
 
        // 选中的要导出的文档id
        String id = request.getParameter("id");
        if (StringUtils.isBlank(id))
        {
            return;
        }
 
        // 1.根据id找出文档信息
        DocumentTaskModel taskModel = documentTaskDao.findById(Integer.valueOf(id));
 
        // 2.下载
        File file = new File(taskModel.getUrl());// taskModel.getUrl():该PDF文件的存放路径
        try (FileInputStream in = new FileInputStream(file); ServletOutputStream out = response.getOutputStream();)
        {
            // 若未定义文件名,则使用文档配置中的默认文档名
            String fileName = StringUtils.isNotBlank(taskModel.getName()) ? taskModel.getName()
                    : ReportFormEngine.getDocumentByKey(taskModel.getDocumentKey()).getName();
 
            response.reset();
            response.setContentType("application/x-msdownload");
            response.setHeader("Content-Length", "" + file.length());
            response.addHeader("Content-Disposition",
                    "attachment; filename=" + new String((fileName + ".pdf").getBytes("utf-8"), "iso8859-1"));
 
            byte[] buff = new byte[CommonConstants.BYTE_BUFFER_LENGTH_5120];
            int hasRead = -1;
            while ((hasRead = in.read(buff, 0, buff.length)) > 0)
            {
                out.write(buff, 0, hasRead);
            }
        }
        catch (FileNotFoundException e)
        {
            logger.error(e);
        }
        catch (IOException e)
        {
            logger.error(e);
        }
        catch (Exception e)
        {
            logger.error(taskModel.getUrl() + "下载文件失败!", e);
        }
    }

ReportFormUtil
package com.smartsecuri.bp.cbb.reportform;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
 
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
 
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import com.smartsecuri.bp.cbb.exception.BusinessException;
import com.smartsecuri.bp.cbb.utils.file.FileUtils;
import com.smartsecuri.bp.cbb.utils.other.CommonUtils;
import com.smartsecuri.bp.cbb.utils.system.IoUtils;
 
/**
 * ClassName : ReportFormUtil <br/>
 * Function : (PDF文档相关-主要用于生成报表) <br/>
 * date : 2022年11月29日 下午5:32:18 <br/>
 *
 * @author 
 * @version 
 * @since JDK 1.8
 */
public class ReportFormUtil
{
    private static final Logger logger = LogManager.getLogger(ReportFormUtil.class);
 
    /**
     * DEFAULT_LEADING : (默认行间距).
     */
    public static final float DEFAULT_LEADING = 16f;
    /**
     * fontSize_normal : (正文字体大小11号).
     */
    public static final float FONTSIZE_NORMAL = 11f;
    /**
     * fontSize_titile : (标题字体大小14号).
     */
    public static final float FONTSIZE_TITILE = 14f;
    /**
     * FONTSIZE_COVER : (封面字体大小32号).
     */
    public static final float FONTSIZE_COVER = 32f;
 
    /**
     * normalFont : (通用字体样式:宋体、11号).
     */
    private static Font normalFont = null;
    /**
     * titleFont : (通用标题字体样式:宋体、14号、加粗).
     */
    private static Font titleFont = null;
    /**
     * coverFont : (通用封面字体样式:宋体、28号、加粗).
     */
    private static Font coverFont = null;
 
    // windows 测试环境
    private static final String phantomPath = "G:\\test\\phantomjs-2.1.1-windows\\bin\\phantomjs.exe";
    private static final String JSpath = "G:\\test\\echarts-convert\\echarts-convert.js";
 
    // linux 环境
    // private static String phantomPath = "/usr/local/phantomjs/bin/phantomjs";
    // private static String JSpath = PathUtils.getProjectPath() + "common/reportform/echarts-convert.js";
 
    /**
     * getBaseFont : (获取可以解析中文的字体:使用宋体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static BaseFont getBaseFontChinese()
    {
        try
        {
            // 宋体资源文件路径
            URL path = ReportFormUtil.class.getResource("/config/fonts/simsun.ttc");
            return BaseFont.createFont(path + ",0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
 
            // 本地main方法测试:使用windows自带的宋体文件
            //return BaseFont.createFont("C://Windows//Fonts//simsun.ttc,0", BaseFont.IDENTITY_H, false);
        }
        catch (Exception e)
        {
            logger.error("设置字体样式失败", e);
            return null;
        }
    }
 
    /**
     * getNormalFont : (获取普通正文字体样式). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getNormalFont()
    {
        if (normalFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            normalFont = new Font(bfChinese, FONTSIZE_NORMAL, Font.NORMAL);
        }
        return normalFont;
    }
 
    /**
     * getTitleFont : (获取标题通用字体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getTitleFont()
    {
        if (titleFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            titleFont = new Font(bfChinese, FONTSIZE_TITILE, Font.BOLD);
        }
        return titleFont;
    }
 
    /**
     * getTitleFont : (获取封面通用字体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getCoverFontFont()
    {
        if (coverFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            coverFont = new Font(bfChinese, FONTSIZE_COVER, Font.BOLD);
        }
        return coverFont;
    }
 
 
    /**
     * genFrontCover : (构建封面的文字和样式). <br/>
     *
     * @author 
     * @param coverName 封面标题
     * @param subtitle 小标题——封面标题下一行文字,可以为null或空串,表示不填
     * @param subscript 下标,可以为null或空串,表示不填
     * @return
     * @since JDK 1.8
     */
    public static Paragraph genFrontCover(String coverName, String subtitle, String subscript)
    {
        if (StringUtils.isBlank(coverName))
        {
            return null;
        }
        // 生成封面
        Paragraph frontCover = new Paragraph();
        frontCover.setAlignment(Element.ALIGN_CENTER);
        // 空10行
        ReportFormUtil.addBlankLine(frontCover, Integer.parseInt("10"));
 
        // 封面标题
        frontCover.add(new Chunk(coverName, ReportFormUtil.getCoverFontFont()));
 
        if (StringUtils.isNotBlank(subtitle))
        {
            ReportFormUtil.addBlankLine(frontCover, Integer.parseInt("2"));// 换行
            // 小标题
            frontCover.add(new Chunk(subtitle, ReportFormUtil.getTitleFont()));
        }
 
        if (StringUtils.isNotBlank(subscript))
        {
            // 换行
            ReportFormUtil.addBlankLine(frontCover, Integer.parseInt("25"));
            // companyName公司签名如:"慧盾信息安全科技(苏州)股份有限公司"
            frontCover.add(new Chunk(subscript, ReportFormUtil.getNormalFont()));
        }
        return frontCover;
    }
 
    /**
     * addBlankLine : (添加换行). <br/>
     *
     * @author 
     * @param paragraph 需要添加空行的段落
     * @param lineNum 需要添加空行的个数
     * @since JDK 1.8
     */
    public static void addBlankLine(Paragraph paragraph, int lineNum)
    {
        if (paragraph == null)
        {
            return;
        }
 
        for (int i = 0; i < lineNum; i++)
        {
            paragraph.add(Chunk.NEWLINE);
        }
    }
 
    /**
     * createTable : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param list 构建table的数据
     * @param title 该段落取的名字,标题默认居左
     * @param methodNames 需要调用的方法名,用来获取单元格数据。通常是某个属性的get方法
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> list, String title, String[] tableHead, String[] methodNames,
            String prefixDescribe, String suffixDescribe)
    {
        return createTable(list, FONTSIZE_NORMAL, FONTSIZE_TITILE, title, tableHead, methodNames,
                prefixDescribe, suffixDescribe);
    }
 
    /**
     * createTableByList : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param listData 
     * @param normalFontSize 正文字体大小
     * @param titleFontSize 标题字体大小
     * @param title 段落名称
     * @param methodNames 获取表格属性的方法名
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> listData, float normalFontSize, float titleFontSize, String title,
            String[] tableHead, String[] methodNames, String prefixDescribe, String suffixDescribe)
    {
 
        // 1.创建一个表格
        PdfPTable table = new PdfPTable(methodNames.length);// 列数
 
        // 2.构造表头
        for (String head : tableHead)
        {
            head = StringUtils.isBlank(head) ? "" : head;
            PdfPCell cell = new PdfPCell(new Phrase(head, getNormalFont()));
            cell.setBackgroundColor(
                    new BaseColor(Integer.parseInt("124"), Integer.parseInt("185"), Integer.parseInt("252")));// 背景色
            cell.setMinimumHeight(Float.parseFloat("15"));// 单元格最小高度
            cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
            cell.setVerticalAlignment(Element.ALIGN_CENTER);// 垂直居中
            table.addCell(cell);
        }
 
        // 3.构造table数据
        if (CollectionUtils.isEmpty(listData))
        {
            // 没有数据,添加一行空单元格,并返回
            for (int i = 0; i < methodNames.length; i++)
            {
                table.addCell(new Phrase(" "));// 有一个空格,否则添加不了
            }
        }
        else
        {
            // 有数据:构造table数据
            for (T li : listData)
            {
                for (String name : methodNames)
                {
                    Object obj = CommonUtils.invokeMethod(li, name);
                    String valueStr = obj == null ? " " : StringUtils.isEmpty(obj.toString()) ? " " : obj.toString();
                    PdfPCell cell = new PdfPCell(new Phrase(valueStr, getNormalFont()));
                    cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
                    table.addCell(cell);
                }
            }
        }
 
        // 4.返回段落
        return createParagraph(table, title, prefixDescribe, suffixDescribe);
    }
    
    /**
     * addDataToTable : (从段落中找到table元素,向该table中追加数据). <br/>
     *
     * @author 
     * @param <T>
     * @param paragraph table表格段落
     * @param listData 需要追加的数据
     * @param methodNames 每个数据获取的方法名
     * @since JDK 1.8
     */
    public static <T> void addDataToTable(Paragraph paragraph, List<T> listData, List<String> methodNames)
    {
        for (Element ele : paragraph)
        {
            if (!(ele instanceof PdfPTable))
            {
                // 不是table元素,直接跳过
                continue;
            }
 
            // 找到第一个table元素
            PdfPTable table = (PdfPTable) ele;
            for (T data : listData)
            {
                for (String name : methodNames)
                {
                    String valueStr = CommonUtils.invokeMethod(data, name).toString();
                    PdfPCell cell = new PdfPCell(new Phrase(valueStr, getNormalFont()));
                    cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 单元格文字水平居中
                    table.addCell(cell);
                }
            }
            break;
        }
    }
 
    /**
     * createImage : (根据图片的base64加密文件创建pdf图片段落). <br/>
     *
     * @author 
     * @param picBase64Info 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @param title 段落标题
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * 
     * @return 返回图片段落
     * @since JDK 1.8
     */
    public static Paragraph createImageFromEncodeBase64(String picBase64Info, String title, float percentX,
            float percentY, String prefixDescribe, String suffixDescribe)
    {
        // 1.获取图片
        Element element = analysisPicBase64Info(picBase64Info);
        // 2.创建段落,并添加标题,设置缩放
        return createImageParagraph(element, title, percentX, percentY, prefixDescribe, suffixDescribe);
    }
 
    /**
     * createImageFromEncodeBase64_batch : (批量创建图片段落). <br/>
     *
     * @author 
     * @param picBase64Infos 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @param titles 每个段落的标题
     * @param percentXs X轴缩放比例
     * @param percentYs Y轴缩放比例
     * @param titleCenter 标题是否居中,true-居中、false-默认居左
     * @return 返回由多个图片段落组合后的整个段落
     * @since JDK 1.8
     */
    public static Paragraph createImageFromEncodeBase64_batch(List<String> picBase64Infos, List<String> titles,
            List<Float> percentXs, List<Float> percentYs)
    {
        Paragraph paragraphs = new Paragraph(DEFAULT_LEADING);
        for (int i = 0; i <= picBase64Infos.size(); i++)
        {
            Paragraph imagePara = createImageFromEncodeBase64(picBase64Infos.get(i), titles.get(i), percentXs.get(i),
                    percentYs.get(i), null, null);
            if (!imagePara.isEmpty())
            {
                paragraphs.add(imagePara);
                // 换行
                paragraphs.add(Chunk.NEWLINE);
            }
        }
        return paragraphs;
    }
 
    /**
     * createPicParagraphByPath : (根据图片位置生成图片段落段落). <br/>
     *
     * @author 
     * @param title 段落标题
     * @param picPath 图片所在磁盘路径
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param titleCenter 标题是否居中,true-居中、false-默认居左
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createImageParagraphByPath(String title, String picPath, float percentX, float percentY,
            String prefixDescribe, String suffixDescribe)
    {
        // 1.获取图片
        Element element = analysisPicByPath(picPath);
        // 2.创建段落,并添加标题,设置缩放
        return createImageParagraph(element, title, percentX, percentY, prefixDescribe, suffixDescribe);
    }
 
    /**
     * createPicParagraphByPath : (根据图片位置生成图片段落段落). <br/>
     *
     * @author 
     * @param picElement 图片元素
     * @param title 段落标题
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createImageParagraph(Element picElement, String title, float percentX, float percentY,
            String prefixDescribe, String suffixDescribe)
    {
        if (picElement == null)
        {
            return new Paragraph();
        }
 
        try
        {
            if (!(picElement instanceof Image))
            {
                // 1. 图片解析失败
                logger.error(title + ":picElement is not instanceof Image");
                return new Paragraph();
            }
 
            // 2.设置图片缩放比例
            Image image = (Image) picElement;
            image.scalePercent(percentX, percentY);
            image.setAlignment(Element.ALIGN_CENTER);
 
            // 3.创建并返回图片段落
            return createParagraph(image, title, prefixDescribe, suffixDescribe);
        }
        catch (Exception e)
        {
            logger.error(e);
            // 空段落
            return new Paragraph();
        }
    }
 
    /**
     * createTxtParagraph : (创建文本段落). <br/>
     *
     * @author 
     * @param strings 多行句子
     * @param title 段落标题
     * @param titleCenter 标题居中
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createTxtParagraph(List<String> strings, String title, String prefixDescribe,
            String suffixDescribe)
    {
        Phrase phrase = new Phrase();
        for (String li : strings)
        {
            // 多行句子拼装
            phrase.add(new Chunk(li, getNormalFont()));
            phrase.add(Chunk.NEWLINE);
        }
 
        return createParagraph(phrase, title, prefixDescribe, suffixDescribe);
    }
 
    /**
     * createParagraph : (根据元素创建段落). <br/>
     *
     * @author 
     * @param element
     * @param title 段落标题
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createParagraph(Element element, String title, String prefixDescribe,
            String suffixDescribe)
    {
        title = StringUtils.isEmpty(title) ? "" : title;
        try
        {
            // 1.创建段落,并添加标题,添加前缀描述
            Paragraph paragraph = createParagraph(title, prefixDescribe);
            paragraph.add(element);
 
            // 2.后缀描述
            if (StringUtils.isNotBlank(suffixDescribe))
            {
                addBlankLine(paragraph, 1);// 换行符
                paragraph.add(new Paragraph(DEFAULT_LEADING, suffixDescribe, getNormalFont()));
            }
            return paragraph;
        }
        catch (Exception e)
        {
            logger.error(e);
            // 空段落
            return new Paragraph();
        }
    }
 
    /**
     * createParagraph : (创建段落). <br/>
     *
     * @author 
     * @param title
     * @param prefixDescribe 前缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createParagraph(String title, String prefixDescribe)
    {
        Paragraph paragraph = new Paragraph(DEFAULT_LEADING);
 
        if (StringUtils.isNotEmpty(title))
        {
            paragraph.add(new Phrase(DEFAULT_LEADING, title, getTitleFont()));
            addBlankLine(paragraph, 2);
        }
 
        // 2.前缀描述
        if (StringUtils.isNotBlank(prefixDescribe))
        {
            paragraph.add(new Paragraph(prefixDescribe, getNormalFont()));
            ReportFormUtil.addBlankLine(paragraph, Integer.parseInt("1"));// 换行
        }
 
        return paragraph;
    }
 
    /**
     * createPDFDocument : (创建文档,默认纸张大小A4). <br/>
     *
     * @author WuTingTing
     * @return
     * @since JDK 1.8
     */
    public static Document createPDFDocument()
    {
        return new Document(PageSize.A4, Float.parseFloat("36"), Float.parseFloat("36"), Float.parseFloat("36"),
                Float.parseFloat("36"));
    }
 
    /**
     * createPDFDocumentToDisk : (生成PDF文档保存到磁盘). <br/>
     *
     * @author WuTingTing
     * @param coverName 封面标题
     * @param subtitle 小标题——封面标题下一行文字,可以为null或空串,表示不填
     * @param subscript 下标,可以为null或空串,表示不填
     * @param paragraphs 段落
     * @param documentPath 文件保存全路径
     * @return
     * @since JDK 1.8
     */
    public static boolean createPDFDocumentToDisk(String coverName, String subtitle, String subscript,
            List<Paragraph> paragraphs, String documentPath)
    {
        // 1.创建文档,设置文档页面大小,页边距
        Document document = createPDFDocument();
        // 2.封面
        Paragraph cover = genFrontCover(coverName, subtitle, subscript);
 
        // 3.生成文档并保存到指定路径
        return downloadDocument(document, cover, paragraphs, documentPath);
    }
 
    /**
     * exportDocument : (生成并下载PDF文档-通过response响应实现下载). <br/>
     *
     * @author
     * @param document 文档对象
     * @param cover 封面:若不是null,则会先添加封面,并另起新页面添加段落;若是null表示没有封面。
     * @param paragraphs 需要组成PDF文件的各个段落
     * @param response 请求的响应对象(提示:前台无法通过ajax请求触发浏览器的下载,可以通过表单提交的方式)
     * @param fileName 生成的文件名称,不需要加pdf后缀
     * @since JDK 1.8
     */
    public static void exportDocument(Document document, Paragraph cover, List<Paragraph> paragraphs,
            HttpServletResponse response, String fileName)
    {
        try (ServletOutputStream out = response.getOutputStream())
        {
            response.setContentType("application/binary;charset=UTF-8");
            response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileName + ".pdf", "UTF-8"));
 
            PdfWriter writer = PdfWriter.getInstance(document, out);
            writer.setStrictImageSequence(true);// 设置图片位置精确放置
            // 1.打开文档
            document.open();
 
            if (cover != null)
            {
                // 2.有封面:添加封面,并另起一页,用来塞后面的段落
                document.add(cover);
                document.newPage(); // 另起一页
            }
 
            StringBuilder errorMsg = new StringBuilder();
            for (int i = 0; i < paragraphs.size(); i++)
            {
                try
                {
                    // 将段落添加到文档
                    document.add(paragraphs.get(i));
                    // 换行
                    document.add(Chunk.NEWLINE);
                }
                catch (DocumentException e)
                {
                    errorMsg.append("PDF文件生成出错,请检查第:").append(i).append("个段落");
                }
            }
 
            if (!StringUtils.isEmpty(errorMsg.toString()))
            {
                logger.error(errorMsg);
            }
 
            // 关闭文档
            document.close();
            // 将数据输出
            out.flush();
            out.close();
        }
        catch (IOException e)
        {
            logger.error("生成PDF文档并下载,IOException:", e);
        }
        catch (DocumentException e)
        {
            logger.error("生成PDF文档并下载,DocumentException:", e);
        }
        finally
        {
            document.close();
        }
    }
 
    /**
     * downloadDocument : (生成PDF文档并保存到磁盘). <br/>
     *
     * @author 
     * @param document 文档对象
     * @param cover 封面:若不是null,则会先添加封面,并另起新页面添加段落
     * @param paragraphs 需要组成PDF文件的段落
     * @param response 请求的响应对象
     * @param fileName 生成的文件名称,不需要加pdf后缀
     * @return true成功、false失败
     * @since JDK 1.8
     */
    public static boolean downloadDocument(Document document, Paragraph cover, List<Paragraph> paragraphs,
            String documentPath)
    {
        FileOutputStream out = null;
        try
        {
            File file = new File(documentPath);
            if (!FileUtils.createFile(file))
            {
                return false;
            }
 
            out = new FileOutputStream(file);
            PdfWriter writer = PdfWriter.getInstance(document, out);
            writer.setStrictImageSequence(true);// 设置图片位置精确放置
 
            // 打开文档
            document.open();
 
            if (cover != null)
            {
                document.add(cover);
                // 起新页面
                document.newPage();
            }
 
            StringBuilder errorMsg = new StringBuilder();
            for (int i = 0; i < paragraphs.size(); i++)
            {
                try
                {
                    // 将段落添加到文档
                    document.add(paragraphs.get(i));
                    // 换行
                    document.add(Chunk.NEWLINE);
                }
                catch (DocumentException e)
                {
                    errorMsg.append("PDF文件生成出错,请检查第:").append(i).append("个段落");
                }
            }
 
            if (!StringUtils.isBlank(errorMsg.toString()))
            {
                logger.error(errorMsg);
            }
 
            // 关闭文档
            document.close();
            out.flush();
            IoUtils.close(out);
        }
        catch (Exception e)
        {
            logger.error("生成PDF文档并下载,出错:", e);
            return false;
        }
        finally
        {
            // 关闭文档
            document.close();
            IoUtils.close(out);
        }
        return true;
    }
 
    /**
     * analysisPicBase64Info : (解析base64图片信息). <br/>
     *
     * @author 
     * @param picBase64Info 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @return 图片经过base64解码后的信息
     * @since JDK 1.8
     */
    public static Element analysisPicBase64Info(String picBase64Info)
    {
        if (StringUtils.isEmpty(picBase64Info))
        {
            // 空段落
            return new Paragraph();
        }
 
        // 1.获取图片base64字符串信息:若入参是通过前台echarts调用getDataURL()方法获取的,则该字符串包含逗号,且则逗号后面的内容才是图片的信息
        String pictureInfoStr = picBase64Info.indexOf(",") == -1 ? picBase64Info : picBase64Info.split(",")[1];
        // 2.将图片信息进行base64解密
        byte[] imgByte = Base64.decodeBase64(pictureInfoStr);
 
        // 对异常的数据进行处理
        /**
         * .图片的原始表达ascii码范围是0-255,
         * .这里面有一些不可见的编码。然后为了图片正确传输才转成编码base64的0-63,
         * .当从base64转成byte时,byte的范围是[-128,127],那么此时就会可能产生负数,而负数不是在ascii的范围里,所以需要转换一下
         */
        for (int i = 0; i < imgByte.length; i++)
        {
            if (imgByte[i] < 0)
            {
                imgByte[i] += 256;
            }
        }
 
        try
        {
            return Image.getInstance(imgByte);
        }
        catch (Exception e)
        {
            logger.error("analysisPicBase64Info error", e);
            return new Paragraph();
        }
    }
 
    /**
     * analysisPicBase64Info : (根据图片地址解析并生成图片段落). <br/>
     *
     * @author 
     * @param picPath 图片路径
     * @return 
     * @since JDK 1.8
     */
    public static Element analysisPicByPath(String picPath)
    {
        if (StringUtils.isEmpty(picPath))
        {
            return null;// 空段落
        }
 
        File file = new File(picPath);
        if (!file.exists())
        {
            // 图片文件不存在
            return null;// 空段落
        }
 
        try (FileInputStream in = new FileInputStream(file))
        {
            byte[] imgByte = new byte[(int) file.length()];
            in.read(imgByte);
            /**
             * .图片的原始表达ascii码范围是0-255,
             * .这里面有一些不可见的编码。然后为了图片正确传输才转成编码base64的0-63,
             * .当从base64转成byte时,byte的范围是[-128,127],那么此时就会可能产生负数,而负数不是在ascii的范围里,所以需要转换一下
             */
            for (int i = 0; i < imgByte.length; i++)
            {
                if (imgByte[i] < 0)
                {
                    imgByte[i] += 256;
                }
            }
 
            return Image.getInstance(imgByte);
        }
        catch (Exception e)
        {
            logger.error("analysisPicBase64Info error", e);
            return null;
        }
    }
 
    /**
     * analysisPicBase64Info_batch : (批量解析base64加密的图片信息,生成Image对象). <br/>
     *
     * @author 
     * @param picBase64Infos 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @return
     * @since JDK 1.8
     */
    public static List<Element> analysisPicBase64Info_batch(List<String> picBase64Infos)
    {
        List<Element> images = new ArrayList<Element>();
        for (String li : picBase64Infos)
        {
            Element image = analysisPicBase64Info(li);
            images.add(image);
        }
        return images;
    }
 
    /**
     * doExecPhantomJS_deleteFileAfterException : (执行:浏览器加载option,渲染并截图保存到制定文件,若过程中出现错误则删除picTmpPath、picPath中生成的文件). <br/>
     * .【注意】支持一次多个,多个时路径以逗号隔开,且jsonPath和picPath的个数要一致。
     */
    public static void doExecPhantomJS_deleteFileAfterException(String txtPath, String picTmpPath, String picPath)
            throws BusinessException, Exception
    {
        doExecPhantomJS_deleteFileAfterException(txtPath, picTmpPath, picPath, null, null);
    }
    
    /**
     * doExecPhantomJS_deleteFileAfterException : (执行:浏览器加载option,渲染并截图保存到制定文件,若过程中出现错误则删除picTmpPath、picPath中生成的文件). <br/>
     * .【注意】支持一次多个,多个时路径以逗号隔开,且jsonPath和picPath的个数要一致。
     * .单个例子:
     * txtPath="/a/a.json"
     * picTmpPath="/a/tmp/a.png"
     * picPath="/a/a.png"
     * width=1000
     * height=600
     * 
     * .多个例子:
     * txtPath="/a/a.txt,/b/b.json,/c/c.txt"
     * picTmpPath="/a/tmp/a.png,/b/tmp/b.png,/c/tmp/c.png"
     * picPath="/a/a.png,/b/b.png,/c/c.png"
     * width=1000,1000,1000
     * height=600,600,600,600
     *
     * @author WuTingTing
     * @param txtPath 图表option信息的txt文件全路径
     * @param picTmpPath 图片临时存放路径,生成后将转移到picPath路径下
     * @param picPath 图片文件全路径
     * @param width 自定义截图宽度
     * @param height 自定义截图高度
     * @throws Exception 
     * @since JDK 1.8
     */
    public static void doExecPhantomJS_deleteFileAfterException(String txtPath, String picTmpPath, String picPath,
            String width, String height) throws BusinessException, Exception
    {
        try
        {
            doExecPhantomJS(txtPath, picTmpPath, picPath, width, height);
        }
        catch (Exception e)
        {
            // 执行过程中出错,删除本次执行中可能生成的文件,不删除txtPath是防止该文件为默认内置文件
            String[] picTmpPaths = picTmpPath.split(",");
            String[] picPaths = picPath.split(",");
 
            for (String li : picTmpPaths)
            {
                FileUtils.delFile(li);
            }
 
            for (String li : picPaths)
            {
                FileUtils.delFile(li);
            }
 
            if (e instanceof BusinessException)
            {
                logger.error(((BusinessException) e).getMsg());
                throw new BusinessException().setMsg(((BusinessException) e).getMsg());
            }
            else
            {
                throw new Exception(e);
            }
        }
    }
 
    /**
     * doExecPhantomJS : (执行:浏览器加载option,渲染并截图保存到制定文件). <br/>
     * .【注意】支持一次多个,多个时路径以逗号隔开,且jsonPath和picPath的个数要一致。
     * .单个例子:
     * txtPath="/a/a.json"
     * picTmpPath="/a/tmp/a.png"
     * picPath="/a/a.png"
     * 
     * .多个例子:
     * txtPath="/a/a.txt,/b/b.json,/c/c.txt"
     * picTmpPath="/a/tmp/a.png,/b/tmp/b.png,/c/tmp/c.png"
     * picPath="/a/a.png,/b/b.png,/c/c.png"
     *
     * @author WuTingTing
     * @param txtPath 图表option信息的txt文件全路径
     * @param picTmpPath 图片临时存放路径,生成后将转移到picPath路径下
     * @param picPath 图片文件全路径
     * @throws Exception 
     * @since JDK 1.8
     */
    private static void doExecPhantomJS(String txtPath, String picTmpPath, String picPath, String width, String height)
            throws BusinessException, Exception
    {
        String cmd = getCMD(txtPath, picTmpPath, picPath, width, height);
        logger.info("图片生成命令:" + cmd);
 
        BufferedReader processInput = null;
 
        // 检查文件是否存在
        boolean existP = FileUtils.fileExists(phantomPath);
        boolean existJ = FileUtils.fileExists(JSpath);
        if (!existP || !existJ)
        {
            throw new BusinessException()
                    .setMsg("生成图片必要文件缺失:" + (!existP ? "phantomjs " : "") + (!existJ ? "echarts-convert.js、" : ""));
        }
 
        try
        {
            Process process = Runtime.getRuntime().exec(cmd);
            processInput = new BufferedReader(
                    new InputStreamReader(process.getInputStream(), Charset.forName("UTF-8")));
            String line = "";
            while ((line = processInput.readLine()) != null)
            {
                // 执行信息打印
                logger.info(line);
            }
            int waitFor = process.waitFor();
            if (waitFor != 0)
            {
                logger.info("图片生成过程,非正常退出,请检查是否存在异常");
            }
        }
        finally
        {
            if (processInput != null)
            {
                try
                {
                    processInput.close();
                }
                catch (IOException e)
                {
                    logger.error("io close fail");
                }
            }
        }
    }
 
    /**
     * getCMD : (拼接命令). <br/>
     *
     * @author 
     * @param txtPath 图表option信息的txt文件全路径
     * @param picTmpPath 图片临时存放路径,生成后将转移到picPath路径下
     * @param picPath 图片文件全路径
     * @param width 自定义截图宽度
     * @param height 自定义截图高度
     * @return
     * @throws BusinessException
     * @since JDK 1.8
     */
    private static String getCMD(String txtPath, String picTmpPath, String picPath, String width, String height)
            throws BusinessException
    {
        if (StringUtils.isBlank(txtPath) || StringUtils.isBlank(picTmpPath) || StringUtils.isBlank(picPath))
        {
            logger.error("txtPath or picTmpPath or picPath is blank");
            throw new BusinessException().setMsg("doExecPhantomJS 入参错误");
        }
 
        if (StringUtils.isNotBlank(width) && StringUtils.isNotBlank(height))
        {
            return phantomPath + " " + JSpath + " -txtPath " + txtPath + " -picTmpPath " + picTmpPath + " -picPath "
                    + picPath + " -width " + width + " -height " + height;
        }
        else
        {
            // 未自定义截图宽度和高度,会使用默认宽、高
            return phantomPath + " " + JSpath + " -txtPath " + txtPath + " -picTmpPath " + picTmpPath + " -picPath "
                    + picPath;
        }
    }
 
    public static void main(String[] args)
    {
        try
        {
            // 测试同时生成两张图
            // 注意:要先手动创建两个空文件G:\\test\\1.png和G:\\test\\2.png;要提前将echarts的option数据写到G:\\test\\testOption.txt和G:\\test\\testOption2.txt中
            doExecPhantomJS_deleteFileAfterException("G:\\test\\testOption.txt,G:\\test\\testOption2.txt", "G:\\test\\1.png,G:\\test\\2.png", "G:\\test\\111.png,G:\\test\\222.png");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
 
        Paragraph ph1 = ReportFormUtil.createImageParagraphByPath("1、段落标题111", "G:\\test\\111.png", 35, 35,"这是一段前缀描述信息111", "这是一段后缀描述信息111");
        Paragraph ph2 = ReportFormUtil.createImageParagraphByPath("2、段落标题222", "G:\\test\\222.png", 35, 35, "这是一段前缀描述信息222", "这是一段后缀描述信息222");
 
        List<Paragraph> phs = new ArrayList<Paragraph>();
        phs.add(ph1);
        phs.add(ph2);
 
        ReportFormUtil.createPDFDocumentToDisk("封面名称", "小标题", "", phs, "G:\\test\\document.pdf");
    }
}

 

 

本文出自:https://blog.csdn.net/qq_41773784/article/details/131592864

七种常用的设计模式

常用的七种设计模式:单例模式、工厂方法模式、抽象工厂模式、代理模式、装饰器模式、观察者模式和责任链模式。

设计模式分类

设计模式根据工作的目的,分为创建型模式、结构型模式和行为型模式三类。

创建型模式:单例模式、工厂方法模式、抽象工厂模式、创建者模式、原型模式。

结构型模式:适配器模式、代理模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

软件设计七大原则(OOP原则)

开闭原则:对扩展开放,对修改关闭。
里氏替换原则:不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义。
依赖倒置原则:要面向接口编程,不要面向实现编程。
单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性。
接口隔离原则:要为各个类建立它们需要的专用接口。
迪米特法则:一个类应该保持对其它对象最少的了解,降低耦合度。
合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

实际上,七大原则的目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。

1、单例模式:一个类只有一个实例,且该类能自行创建这个实例的一种模式
①单例类只有一个实例对象

②该单例对象必须由单例类自行创建

③单例类对外提供一个访问该单例的全局访问点

④、优点
单例模式可以保证内存里只有一个实例,减少了内存的开销。
可以避免对资源的多重占用。
单例模式设置全局访问点,可以优化和共享资源的访问。
⑤、缺点
单例模式一般没有接口,扩展困难。
单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则

饿汉式单例:类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在,这种饿汉式单例会造成空间浪费。

public class Hungry {
    private Hungry(){}
    private final static Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

懒汉式单例:为了避免内存空间浪费,采用懒汉式单例,即用到该单例对象的时候再创建。

public class LazyMan {
    private LazyMan(){};
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

这是最简单的懒汉式,但是,存在很大问题,单线程下这段代码没有问题,但是在多线程下有很大问题。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"");
    }
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
 
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

会发现结果都不一样,因此,并发情况下,这段代码是有问题的。我们需要进行两端检测,进行“加锁”:synchronized (Singleton.class)。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"");
    }
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){        //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
            synchronized (LazyMan.class){    //第一层锁,保证只有一个线程进入
                //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
                //当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
                //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
                if(lazyMan==null){    //第二层检查
                    //synchronized关键字作用为禁止指令重排,保证返回Singleton对象一定在创建对象后
                    lazyMan = new LazyMan();        //这行代码存在的问题,不能保证原子性
实际上会执行以下内容:
                    //(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
                    //假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),当单例模式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;但在多线程情况下,假如线程1执行lazyMan = new LazyMan()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤2,但此时已有引用指向对象也就是lazyMan!=null,故线程2在第一次检查时不满足条件直接返回lazyMan,此时lazyMan为null
                    //synchronized关键字可保证lazyMan = new LazyMan()语句执行顺序为123,因其为非原子性依旧可能存在系统调度问题(即执行步骤时被打断),但能确保的是只要lazyMan!=0,就表明一定执行了属性初始化操作;而若在步骤3之前被打断,此时lazyMan依旧为null,其他线程可进入第一层检查向下执行创建对象
                }
            }
        }
        return lazyMan;
    }
 
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

可以看到结果都是只有一个,按理来说是没有问题,实际上不是,上述标注行代码存在问题的,不是一个原子性操作。

静态内部类单例:是不安全,存在问题的,是不安全,存在问题的(修改过的懒汉式单例也是如此)。

public class Inner {
    private Inner(){}
    public static Inner getInstance(){
        return InnerClass.INNER;
    }
    public static class InnerClass{
        private static final Inner INNER = new Inner();
    }
}

2、工厂方法模式:实例化对象不是用new,用工厂方法替代。将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

简单工厂模式:用来生产同一等级架构中的任意产品(对于增加新的产品,需要修改已有代码)
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例。

接下来创建一个接口,两个实现类,一个工厂,一个测试类

//创建手机接口
public interface Phone {
    void name();
}
//创建华为实现类
public class HuaWei implements Phone{
    @Override
    public void name() {
        System.out.println("华为手机");
    }
}
//创建小米实现类
public class XiaoMi implements Phone{
    @Override
    public void name() {
        System.out.println("小米手机");
    }
}
//创建工厂
public class PhoneFactory {
    public static Phone getPhone(String phone){
        if(phone.equals("华为")){
            return new HuaWei();
        }else if(phone.equals("小米")){
            return  new XiaoMi();
        }else {
            return null;
        }
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Phone p1= PhoneFactory.getPhone("华为");
        Phone p2= PhoneFactory.getPhone("小米");
        p1.name();
        p2.name();
    }
}

得到测试结果

        华为手机
        小米手机

我们通过创建一个PhoneFactory类,成功的完成工厂的创建。我们在创建对象时,也就不需要直接创建对象,而是可以通过创建工厂,这样大大的降低了代码的耦合性。但是,静态工厂模式是不能添加数据的。比如说,我们想添加一个“Oppo”手机类,你不直接修改PhoneFactory工厂代码,是不能实现的。所以,就有了第二种的工厂方法模式。

工厂方法模式:用来生产同一等级架构中的固定产品,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。(支持增加任意产品)

//创建手机接口
public interface Phone {
    void name();
}
//创建华为实现类
public class HuaWei implements Phone{
    @Override
    public void name() {
        System.out.println("华为手机");
    }
}
//创建手机工厂接口
public interface PhoneFactory {
    Phone getPhone();
}
//创建华为工厂
public class HuaWeiFactory implements PhoneFactory{
    @Override
    public Phone getPhone() {
        return new HuaWei();
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Phone phone = new HuaWeiFactory().getPhone();
        phone.name();
    }
}

得到测试结果

        华为手机

我们创建了手机工厂接口PhoneFactory,再创建华为工厂HuaWeiFactory实现工厂,这样就可以通过HuaWeiFactory创建对象。增加新的具体工厂和产品族很方便,比如说,我们想要增加小米,只需要创建一个小米工厂XiaoMiFactory实现手机工厂接口PhoneFactory,合理的解决的简单工厂模式不能修改代码的缺点。但是,在现实使用中,简单工厂模式占绝大多数。

简单工厂模式与工厂方法模式比较:

结构的复杂度:简单工厂模式占优。
代码的复杂度:简单工厂模式占优。
编程的复杂度:简单工厂模式占优。
管理的复杂的:简单工厂模式占优。
因此,虽然简单工厂模式不符合设计模式,但是实际使用远大于工厂方法模式。

3、抽象工厂模式:抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类(围绕一个超级工厂创建其他工厂,该超级工厂称为工厂的工厂)

抽象工厂模式得主要角色:

1、抽象工厂(Abstract Factory)提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
2具体工厂(Concrete Factory)主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
3抽象产品(Product)定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
4具体产品(ConcreteProduct)实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

//电脑接口
public interface Computer {
    void play();
    void watch();
}
//创建华为电脑对象
public class HuaWeiComputer implements Computer{
    @Override
    public void play() {
        System.out.println("HuaWei's play!");
    }
 
    @Override
    public void watch() {
        System.out.println("HuaWei's watch!");
    }
}
//手机接口
public interface Phone {
    void send();
    void call();
}
//创建华为手机对象
public class HuaWeiPhone implements Phone{
    @Override
    public void send() {
        System.out.println("HuaWei's send");
    }
 
    @Override
    public void call() {
        System.out.println("HuaWei's call");
    }
}
//抽象工厂
public interface IProductFactory {
    //生产手机
    Phone phone();
    //生产电脑
    Computer computer();
}
//创建华为工厂
public class HuaWeiFactory implements IProductFactory{
    @Override
    public Phone phone() {
        return new HuaWeiPhone();
    }
 
    @Override
    public Computer computer() {
        return new HuaWeiComputer();
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        HuaWeiFactory huaWeiFactory = new HuaWeiFactory();
        Phone phone = huaWeiFactory.phone();
        phone.call();
        phone.send();
        Computer computer = huaWeiFactory.computer();
        computer.play();
        computer.watch();
    }
}

得到测试结果

        HuaWei's call
        HuaWei's send
        HuaWei's play!
        HuaWei's watch!

我们通过创建一个抽象工厂完成了对具体工厂的创建,只需要传入参数就可以实例化对象。具体产品在应用层的代码隔离,无需关心创建的细节将一个系列的产品统一到一起创建。将一系列产品规划到一起创建。但是,抽象工厂模式也存在着缺点。规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难,不可以增加产品,只能增加品牌。

4、代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

静态代理模式:

角色分析:

1、抽象角色:一般会使用接口或抽象类来解决

2、真实角色:被代理的角色

3、代理角色:代理真实角色,代理真实角色后我们会进行一些附属操作

4、访问角色:访问代理对象的人

//租房
public interface Rent {
    void rent();
}
//房东
public class Master implements Rent{
    @Override
    public void rent() {
        System.out.println("Master rent");
    }
}
//中介
public class Proxy implements Rent{
    private Master master;
 
    public Proxy() {
    }
 
    public Proxy(Master master) {
        this.master = master;
    }
 
    @Override
    public void rent() {
        see();
        master.rent();
        fare();
    }
    //看房
    public void see(){
        System.out.println("see");
    }
    //收费
    public void fare(){
        System.out.println("fare");
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Master master = new Master();
        //进行代理
        Proxy proxy = new Proxy(master);
        //不需要通过对象,直接通过代理完成响应业务
        proxy.rent();
    }
}

得到测试结果

        see
        Master rent
        fare

可以从上述代码看出,我们通过创建中介这一代理完成了租房,并且还有看房、收费的附属操作。我们不需要使用房东对象,通过使用代理中介就可以完成操作。

代理模式优点:可以使真实角色的操作更加纯粹!不用去关注一些公共的业务,公共也就可以交给代理角色,实现了业务的分工,公共业务发生扩展的时候,方便集中管理!
代理模式缺点:一个真实角色就会产生一个代理角色;代码量会翻倍开发效率会变低,也许,这样无法理解到代理模式的好处。举个例子也许能更好理解,比如说我们想要在原有固定功能上新增业务,按照开闭原则我们是不能对原有代码进行修改的。但是我们可以通过代理模式,增加代理,在实现原有功能的情况下写入新的功能,创建对象时也就可以使用代理,完成操作。

动态代理模式:虽然静态代理模式可以很好的解决开闭原则,但是每有一个真实角色就会产生一个代理,代码量翻倍过于臃肿,开发效率较低。因此,我们就使用动态代理模式进行设计。

//接口
public interface IUserService {
    void add();
    void delete();
    void update();
    void query();
 
}
//实现类
public class UserServiceImpl implements IUserService {
    @Override
    public void add() {
        System.out.println("add");
    }
 
    @Override
    public void delete() {
        System.out.println("delete");
    }
 
    @Override
    public void update() {
        System.out.println("update");
    }
 
    @Override
    public void query() {
        System.out.println("query");
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//自动生成动态代理类模板
public class ProxyInvocationHandler implements InvocationHandler {
    //被代理接口
    private Object target;
   
    public void setTarget(Object target) {
        this.target = target;
    }
     //得到代理类
    public Object getProxy() {
        return Proxy.newProxyInstance(getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    public void log(String s) {
        System.out.println("[debug]:" + s);
    }
    //得到代理类
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        ProxyInvocationHandler handler = new ProxyInvocationHandler();
        //设置代理对象
        handler.setTarget(userService);
        //生成代理类
        IUserService proxy = (IUserService)handler.getProxy();
        proxy.add();
        proxy.query();
    }
}

得到测试结果

        [debug]:add
        add
        [debug]:query
        query

通过测试我们可以顺利的使用动态代理模式完成一系列操作,当我们想要添加附属操作时,我们只需要在模板中进行添加。优点:①可以使真实角色的操作更加纯粹!不用去关注一些公共的业务。②公共也就可以交给代理角色!实现了业务的分工。③公共业务发生扩展的时候,方便集中管理。④一个动态代理类代理的是一个接口,一般就是对应的一类业务。⑤一个动态代理类可以代理多个类,只要是实现了同一个接口即可!

5、装饰者模式:动态的将新功能附加到对象上。在对象功能的拓展方面,比继承更有弹性。同时装饰者模式也体现了开闭原则。

角色分析:

1、抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
2、具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
3、抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
4、具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

//定义抽象类
public abstract class Drink {
    public abstract double cost();
}
//定义两个抽象类的实现类
public class Juice extends Drink{
    @Override
    public double cost() {
        System.out.println("juice: "+16);
        return 16;
    }
}
public class Milk extends Drink{
    @Override
    public double cost() {
        System.out.println("milk: "+12);
        return 12;
    }
}
//定义装饰抽象类
public abstract class Decorator extends Drink {
    public abstract double cost();
}
//定义两个装饰具体实现类
public class Chocolate extends Decorator{
    private final static double COST = 4;
    private Drink drink;
 
    public Chocolate(Drink drink) {
        this.drink = drink;
    }
 
    @Override
    public double cost() {
        System.out.println("chocolate:"+4);
        return COST+drink.cost();
    }
}
public class Pudding extends Decorator{
    private final static double COST = 5;
    private Drink drink;
 
    public Pudding(Drink drink) {
        this.drink = drink;
    }
 
    @Override
    public double cost() {
        System.out.println("pudding:"+5);
        return COST+drink.cost();
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        Drink milk = new Milk();
        milk = new Pudding(milk);
        milk = new Chocolate(milk);
        System.out.println(milk.cost());
    }
}

得到测试结果

        chocolate:4
        pudding:5
        milk: 12
        21.0

可以看到非常简单的就能够完成具体构件和具体装饰的组合。也可以看到结构也非常简洁明,具体构件在自己的package中,具体装饰也在自己的package中。我们不管是想要增加具体构件还是具体配饰,都可以在各自的package中添加。对已有的代码不需要进行任何操作,就算新加的有bug也不会影响原有代码的操作。

6、观察者模式:对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式有点:

1、降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。

2、目标与观察者之间建立了一套触发机制。

//定义观察者接口
public interface Observer {
    void response();
}
//具体化观察者1
public class Observer1 implements Observer{
    @Override
    public void response() {
        System.out.println("Observer1 action");
    }
}
//具体化观察者2
public class Observer2 implements Observer{
    @Override
    public void response() {
        System.out.println("Observer2 action");
    }
}
//抽象目标
public abstract class Subject {
    //创建观察者集合
    protected ArrayList<Observer> array = new ArrayList<Observer>();
    //增加观察者
    public void add(Observer observer){
        array.add(observer);
    }
    //删除观察者
    public void remove(Observer observer){
        array.remove(observer);
    }
    //通知观察者方法
    public abstract void notifyObserver();
}
//具体化目标
public class Subject1 extends Subject{
    @Override
    public void notifyObserver() {
        for (Observer observer :array){
            observer.response();
        }
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        Subject subject = new Subject1();
        Observer obs1 = new Observer1();
        Observer obs2 = new Observer2();
        subject.add(obs1);
        subject.add(obs2);
        subject.notifyObserver();
    }
}

得到测试结果:

        Observer1 action
        Observer2 action

通过测试我们可以看到,我们不需要建立太多观察者和具体目标之间的联系,大大降低了目标与观察者之间的耦合关系。并且结构也十分简单明了,观察者和目标分别在各自的package包下。当我们想要添加观察者时,只需要在观察者包下进行添加,实现Observer接口就可以了。

7、责任链模式:一种处理请求的模式,它让多个处理器都有机会处理该诘求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
责任链模式的主要角色
抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
责任链模式优点
降低了对象之间的耦合度。处理者不需要知道客户的任何信息,客户也不要知道处理者是如何实现方法的。
提高了系统的灵活性。当我们想要新增处理器到整个链条中时,所付出的代价是非常小的
责任链模式缺点
降低了系统的性能。对比较长的职责链,请求的处理可能涉及多个处理对象
不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。

//抽象处理者
public abstract class Handler {
    private Handler next;
    public void setNext(Handler next) { this.next = next; }
    public Handler getNext() { return next; }
 
    //处理请求
    public abstract void handleRequest(int info);
}
//具体处理者1
public class Handler1 extends Handler{
    @Override
    public void handleRequest(int info) {
        if (info <10){
            System.out.println("Handler1完成处理");
        }else {
            if (getNext()!=null){
                getNext().handleRequest(info);
            }else {
                System.out.println("没有处理者进行处理");
            }
        }
    }
}
//具体处理者2
public class Handler2 extends Handler{
    @Override
    public void handleRequest(int info) {
        if (info <20&&info>10){
            System.out.println("Handler2完成处理");
        }else {
            if (getNext()!=null){
                getNext().handleRequest(info);
            }else {
                System.out.println("没有处理者进行处理");
            }
        }
    }
}
测试类
public class Test {
    public static void main(String[] args) {
        Handler handler1 = new Handler1();
        Handler handler2 = new Handler2();
        handler1.setNext(handler2);
        handler1.handleRequest(5);
        handler1.handleRequest(15);
        handler1.handleRequest(25);
    }
}

得到测试结果:

Handler1完成处理
Handler2完成处理
没有处理者进行处理

通过测试结果我们看到,5交给了Handler1处理,15交给了Handler2处理,而25则没有处理者处理。请求者根本不需要参与处理,只需要提交数据就可以完成功能的处理,完全不需要管是哪个处理者进行处理的。当我们想要继续添加处理者时,这只需要再次添加就可以了,也不会对之前的代码造成影响。

本文出自:https://blog.csdn.net/a154555/article/details/125612512

Java I/O流概述

Java I/O流概述
引言
在Java编程中,I/O流是一种用于在程序和外部数据源之间传输数据的机制。这些外部数据源可以是文件、网络连接、内存中的数据等。I/O流以字节或字符的形式操作数据,具体取决于您的需求。

I/O流的类型
Java中有两种主要类型的I/O流:

字节流(Byte Streams):字节流用于以字节的形式处理数据,适用于二进制文件和字节数据的读写。主要使用InputStream和OutputStream类来操作字节流。
字符流(Character Streams):字符流用于以字符的形式处理文本数据,适用于文本文件和字符串的读写。主要使用Reader和Writer类来操作字符流。
字节流(Byte Streams)
字节流的基本概念
字节流是处理二进制数据的一种方式。InputStream和OutputStream类是字节流的主要代表。通过字节流,您可以读取和写入字节数组,适用于处理图像、音频等二进制数据。

字节流的应用
字节流的常见应用场景包括:

读取和写入二进制文件。
处理网络数据流。
操作图像和音频文件。

// 示例代码:使用字节流读取和写入文件
try (InputStream inputStream = new FileInputStream("input.txt");
     OutputStream outputStream = new FileOutputStream("output.txt")) {
    int byteData;
    while ((byteData = inputStream.read()) != -1) {
        outputStream.write(byteData);
    }
} catch (IOException e) {
    e.printStackTrace();
}

字符流(Character Streams)
字符流的基本概念
字符流是处理文本数据的一种方式。Reader和Writer类是字符流的主要代表。通过字符流,您可以以字符的形式读取和写入文本数据,适用于处理文本文件。

字符流的应用
字符流的常见应用场景包括:

读取和写入文本文件。
处理CSV、XML等文本格式的数据。
处理字符编码和国际化文本。

// 示例代码:使用字符流读取和写入文本文件
try (Reader reader = new FileReader("input.txt");
     Writer writer = new FileWriter("output.txt")) {
    int charData;
    while ((charData = reader.read()) != -1) {
        writer.write(charData);
    }
} catch (IOException e) {
    e.printStackTrace();
}

缓冲流(Buffered Streams)
缓冲流的介绍
缓冲流用于提高I/O性能。它们在内部维护了一个缓冲区,可以一次性读取或写入多个字节或字符,减少了磁盘或网络访问的次数,从而提高了效率。

示例代码

// 示例代码:使用缓冲流提高文件读取性能
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line);
        writer.newLine(); // 添加换行符
    }
} catch (IOException e) {
    e.printStackTrace();
}

异常处理和资源管理
异常处理
在I/O操作中,可能会发生异常,如IOException。使用try-catch块来捕获和处理这些异常,确保程序能够优雅地处理错误情况。

资源管理(try-with-resources)
Java 7引入了自动资源管理(try-with-resources)机制,可自动关闭打开的流。这可以通过try块中的资源声明来实现,确保资源在退出try块时自动关闭。

// 示例代码:使用try-with-resources自动关闭资源
try (InputStream inputStream = new FileInputStream("input.txt");
     OutputStream outputStream = new FileOutputStream("output.txt")) {
    // 处理流操作
} catch (IOException e) {
    e.printStackTrace();
}

文件和目录操作
文件和目录操作
使用File类,您可以进行各种文件和目录操作,如创建、删除、重命名文件和目录。

// 示例代码:文件和目录操作
File file = new File("myFile.txt");
if (file.exists()) {
    // 文件存在,执行操作
    if (file.delete()) {
        System.out.println("文件删除成功");
    } else {
        System.out.println("文件删除失败");
    }
} else {
    System.out.println("文件不存在");
}

总结
通过本文,我们深入研究了Java中的I/O流。我们介绍了字节流和字符流的基本概念,讨论了缓冲流的性能提升,以及如何处理异常和管理资源。此外,我们还研究了文件和目录操作,使您能够更好地理解如何在Java程序中有效地处理输入和输出。希望这篇文章对您在Java编程中使用I/O流时有所帮助。

本文出自:https://blog.csdn.net/qq_43546721/article/details/133688858

docker-compose一键启动neo4j

下载镜像

docker pull neo4j:3.5.22-community

编写配置文件
参考文档

编写docker-compose.yml文件

version: "3"

services:
    neo4j:
        image: neo4j:3.5.22-community
        container_name: neo4j 
        restart: always
        ports:
            - 7474:7474
            - 7687:7687
        environment:
            - NEO4J_AUTH:neo4j/neofj
            - TZ:Asia/Shanghai
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /root/neo4j/data:/data
            - /root/neo4j/logs:/logs
            - /root/neo4j/conf:/var/lib/neo4j/conf
            - /root/neo4j/import:/var/lib/neo4j/import
        logging:
            driver: "json-file"
            options:
                max-size: "10m"

networks:
    default:
        external:
            name: my-docker-compose-network

一键启动
docker-compose up -d

本文出自:https://blog.csdn.net/weixin_42289273/article/details/133514499

【JAVA】为什么要使用封装以及如何封装

前言

Java的封装指的是在一个类中将数据和方法进行封装,使其可以保护起来,只能在该类内部访问,而不允许外部直接访问和修改。这是Java面向对象编程的三个基本特性之一,另外两个是继承和多态。在此之前我们已经学习过关于继承的概念,今天我们来学习有关封装的内容。

 

 

封装的含义

封装是面向对象编程的一种重要概念,是将数据和对数据的操作封装在一个类中,使得数据对外部的访问受到限制,只能通过类中的公共方法来访问或操作。封装的目的是隐藏类的实现细节,并且保护数据不被随意修改,从而增强了代码的安全性和可维护性。另外,封装也使得代码的扩展和修改更加方便,只需在类内部进行修改而不需要修改其他代码。

 

通过封装,可以提高类的安全性和可维护性,使得类的实现细节被隐藏,只暴露出有限的接口和功能给外部使用,从而减少了类之间的耦合性。同时,封装还可以实现数据隐藏和数据保护,提高程序的可靠性和可扩展性。

 

四种访问控制符

Java中,采用访问控制符来控制类中数据成员和方法的访问权限,主要有四种访问控制符:public、private、protected和默认访问控制符(即不写访问控制符)。

 

public:表示该数据成员或方法可以被其他任何类访问。

private:表示该数据成员或方法只能在当前类内部访问。

protected:表示该数据成员或方法可以在当前类和其子类中访问。

默认访问控制符:表示该数据成员或方法只能在同一个包内的其他类中访问。

封装的两种经典实例

银行账户类

该类的主要属性包括账户名、账号、账户余额等。其中账户余额是一个私有属性,外部无法直接访问。类中提供了一系列操作账户的方法,例如存款、取款、查询余额等。这些方法都能够在保证账户余额正确的情况下,修改账户余额。

public class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        balance += amount;
    }

    public void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
        } else {
            System.out.println("Insufficient balance.");
        }
    }
}

accountNumber: 表示账户号码的字符串。

balance: 表示账户余额的双精度浮点数。

BankAccount(String accountNumber, double balance): 构造函数,用于创建一个新的银行账户对象。

getAccountNumber(): 返回账户号码的方法。

getBalance(): 返回账户余额的方法。

deposit(double amount): 存款方法,用于向账户中添加资金。

withdraw(double amount): 取款方法,用于从账户中扣除资金。如果账户余额不足,将输出一条错误信息。

学生类

public class Student {
    // 属性
    private String name;
    private int age;
    private String gender;
    private String id;

    // 构造方法
    public Student(String name, int age, String gender, String id) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.id = id;
    }

    // 方法
    public void study() {
        System.out.println("学生正在学习");
    }

    public void showInfo() {
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);
        System.out.println("性别:" + gender);
        System.out.println("学号:" + id);
    }

    // Getter和Setter方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

属性:学生类具有姓名、年龄、性别和学号这些属性。

构造方法:学生类具有一个带四个参数的构造方法,用于初始化学生对象。

方法:学生类具有学习方法和展示学生信息的方法。

Getter和Setter方法:学生类具有获取和设置属性值的Getter和Setter方法,用于保护属性的私有性。

文章知识点与官方知识档案匹配,可进一步学习相关知识

本文出自:https://blog.csdn.net/weixin_73602725/article/details/133498865

如何使用 Java 灵活读取 Excel 内容?

在日常工作和学习中,我们经常需要从 Excel 文件中读取数据。对于少量数据,手动复制和粘贴可能是可行的,但是对于大量的数据,这种方法会变得非常低效。在这种情况下,使用 Java 程序自动读取 Excel 文件中的数据将是更好的选择。

 

本教程将介绍如何使用 Java 程序读取 Excel 文件中的数据,包括以下几个方面:

 

了解 Excel 文件格式

选择 Java 的 Excel 文件读取库

读取 Excel 文件中的数据

处理 Excel 文件中的不同数据类型

使用 Java 程序导出 Excel 文件

让我们开始吧!

 

1、了解 Excel 文件格式

在开始编写 Java 程序读取 Excel 文件之前,我们需要了解 Excel 文件格式。Excel 文件是基于 Microsoft 的 OLE2 格式的,这个格式将文件组织成一系列的 “容器” 和 “对象”。Excel 文件的主要容器是 Workbook,它包含一个或多个 Sheet。每个 Sheet 包含一组行和列,这些行和列组成了一个二维的单元格数组。每个单元格可以包含文本、数字、日期、布尔值等不同的数据类型。

 

2、选择 Java 的 Excel 文件读取库

在 Java 中,有多个库可以用于读取 Excel 文件。这些库包括 Apache POI、JExcelApi、EasyXLS、JXL 等。在本教程中,我们将使用 Apache POI 库。

 

Apache POI 是一个 Java API,用于操作 Microsoft 文档格式,包括 Word、Excel 和 PowerPoint。它提供了一组 Java 类,可以用于创建、读取和修改 Microsoft 文档。

 

3、读取 Excel 文件中的数据

在本节中,我们将演示如何使用 Apache POI 库读取 Excel 文件中的数据。

 

在开始之前,我们需要将 Apache POI 库添加到项目的依赖中。您可以通过 Maven 或 Gradle 等构建工具将它们添加到项目中。

 

以下是 Maven 项目的 pom.xml 文件中添加 Apache POI 库的示例代码:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>

以下是 Gradle 项目的 build.gradle 文件中添加 Apache POI 库的示例代码:

dependencies {
    implementation 'org.apache.poi:poi:4.1.2'
}

接下来,我们将演示如何使用 Apache POI 库读取 Excel 文件中的数据。

 

首先,我们需要创建一个 Workbook 对象,该对象表示整个 Excel 文件。在 Apache POI 中,Workbook 对象有三种不同的类型:HSSFWorkbook 表示一个 .xls 文件,XSSFWorkbook 表示一个 .xlsx 文件,SXSSFWorkbook 表示一个大型 .xlsx 文件。我们可以使用 WorkbookFactory.create() 方法根据文件的类型创建一个 Workbook 对象。以下是创建一个 XSSFWorkbook 对象的示例代码:

 

FileInputStream file = new FileInputStream(new File(“path/to/excel/file.xlsx”));

Workbook workbook = WorkbookFactory.create(file);

接下来,我们可以获取一个 Sheet 对象,该对象表示 Excel 文件中的一个工作表。在 Apache POI 中,Sheet 对象由 Workbook 对象的 getSheet() 方法返回。以下是获取名为 “Sheet1” 的 Sheet 对象的示例代码:

 

Sheet sheet = workbook.getSheet(“Sheet1”);

一旦我们获取了 Sheet 对象,我们就可以遍历其中的每一行和每一列,以读取其中的单元格数据。以下是遍历 Sheet 对象中的所有单元格并输出单元格值的示例代码:

Iterator<Row> rowIterator = sheet.iterator();
while (rowIterator.hasNext()) {
    Row row = rowIterator.next();
    Iterator<Cell> cellIterator = row.cellIterator();
    while (cellIterator.hasNext()) {
        Cell cell = cellIterator.next();
        switch (cell.getCellType()) {
            case STRING:
                System.out.print(cell.getStringCellValue() + "\t");
                break;
            case NUMERIC:
                System.out.print(cell.getNumericCellValue() + "\t");
                break;
            case BOOLEAN:
                System.out.print(cell.getBooleanCellValue() + "\t");
                break;
            default:
                System.out.print("\t");
        }
    }
    System.out.println();
}

上述代码中,我们使用了一个嵌套的迭代器来遍历每一行和每一列。在每个单元格中,我们使用 switch 语句根据不同的数据类型执行不同的操作。

 

4、处理 Excel 文件中的不同数据类型

在 Excel 文件中,单元格可以包含不同的数据类型,包括字符串、数字、日期和布尔值等。当我们读取单元格中的数据时,我们需要知道单元格中的数据类型,以正确地处理它们。

 

在 Apache POI 中,我们可以使用 Cell.getCellType() 方法获取单元格的数据类型。该方法返回 CellType 枚举类型的值,可以使用 switch 语句根据不同的枚举值执行不同的操作。

 

以下是使用 switch 语句处理不同数据类型的示例代码:

switch (cell.getCellType()) {
    case STRING:
        System.out.print(cell.getStringCellValue() + "\t");
        break;
    case NUMERIC:
        if (DateUtil.isCellDateFormatted(cell)) {
            System.out.print(cell.getDateCellValue() + "\t");
        } else {
            System.out.print(cell.getNumericCellValue() + "\t");
        }
        break;
    case BOOLEAN:
        System.out.print(cell.getBooleanCellValue() + "\t");
        break;
    default:
        System.out.print("\t");
}

在上述示例代码中,我们首先检查单元格的数据类型是否为字符串。如果是字符串,我们使用 getStringCellValue() 方法获取字符串值。如果单元格的数据类型为数字,我们还需要检查单元格是否包含日期。如果是日期,我们使用 getDateCellValue() 方法获取日期值,否则我们使用 getNumericCellValue() 方法获取数字值。

 

5、使用 Java 程序导出 Excel 文件

除了读取 Excel 文件中的数据外,我们还可以使用 Java 程序创建和导出 Excel 文件。Apache POI 提供了 HSSFWorkbook 和 XSSFWorkbook 两个类,用于创建和操作 Excel 文件。

 

HSSFWorkbook 类用于操作旧版的 .xls 格式的 Excel 文件,而 XSSFWorkbook 类用于操作新版的 .xlsx 格式的 Excel 文件。我们可以根据需要选择合适的类来创建和导出 Excel 文件。

 

以下是创建和导出 Excel 文件的一般步骤:

 

创建一个 Workbook 对象,表示整个 Excel 文件。

创建一个 Sheet 对象,表示 Excel 文件中的一个工作表。

创建 Row 和 Cell 对象,表示 Excel 文件中的行和列。

为每个单元格设置数据和样式。

将 Workbook 对象写入文件或输出流中。

下面,让我们来看一些示例代码,演示如何使用 Java 程序创建和导出 Excel 文件。

 

创建一个 XSSFWorkbook 对象

以下是创建一个 XSSFWorkbook 对象的示例代码:

Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Sheet1");

创建行和单元格

以下是创建行和单元格的示例代码:

Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
cell.setCellValue("Hello, World!");

设置单元格样式

在 Excel 文件中,我们可以为单元格设置不同的样式,包括字体、颜色、对齐方式等。以下是设置单元格样式的示例代码:

CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
cell.setCellStyle(style);

在上述示例代码中,我们首先使用 createCellStyle() 方法创建一个单元格样式对象。然后,我们使用 createFont() 方法创建一个字体对象,并设置其属性。最后,我们将样式应用于单元格。

 

导出 Excel 文件

导出 Excel 文件时,我们可以将 Workbook 对象写入文件或输出流中。以下是将 Workbook 对象写入文件的示例代码:

FileOutputStream fileOut = new FileOutputStream("path/to/excel/file.xlsx");
workbook.write(fileOut);
fileOut.close();

在上述示例代码中,我们首先使用 FileOutputStream 类创建一个文件输出流对象。然后,我们使用 write() 方法将 Workbook 对象写入文件中。最后,我们关闭文件输出流。

 

除了写入文件,我们还可以将 Workbook 对象输出到网络流中,以便在浏览器中下载 Excel 文件。以下是将 Workbook 对象输出到网络流中的示例代码:

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"excel_file.xlsx\"");
OutputStream outputStream = response.getOutputStream();
workbook.write(outputStream);
outputStream.close();

在上述示例代码中,我们首先设置 HTTP 响应的内容类型和文件名。然后,我们使用 getOutputStream() 方法获取网络输出流对象。最后,我们使用 write() 方法将 Workbook 对象写入网络流中,并关闭输出流。

 

完整示例代码

 

下面是一个完整的示例代码,演示如何使用 Java 程序读取和导出 Excel 文件:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 
public class ExcelUtils {
 
    public static void readExcel(String fileName) throws IOException {
        // 创建文件输入流
        FileInputStream fileInputStream = new FileInputStream(new File(fileName));
 
        // 创建工作簿对象
        Workbook workbook = WorkbookFactory.create(fileInputStream);
 
        // 获取第一个工作表
        Sheet sheet = workbook.getSheetAt(0);
 
        // 遍历工作表中的所有行和单元格
        for (Row row : sheet) {
            for (Cell cell : row) {
                // 根据单元格的类型读取数据
                switch (cell.getCellType()) {
                    case STRING:
                        System.out.print(cell.getStringCellValue() + "\t");
                        break;
                    case NUMERIC:
                        System.out.print(cell.getNumericCellValue() + "\t");
                        break;
                    case BOOLEAN:
                        System.out.print(cell.getBooleanCellValue() + "\t");
                        break;
                    case FORMULA:
                        System.out.print(cell.getCellFormula() + "\t");
                        break;
                    case BLANK:
                        System.out.print("\t");
                        break;
                    default:
                        System.out.print("\t");
                }
            }
            System.out.println();
        }
 
        // 关闭文件输入流
        fileInputStream.close();
    }
 
    public static void writeExcel(String fileName) throws IOException {
        // 创建工作簿对象
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("Sheet1");
 
        // 创建行和单元格
        Row row = sheet.createRow(0);
        Cell cell = row.createCell(0);
        cell.setCellValue("Hello, World!");
 
        // 设置单元格样式
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        cell.setCellStyle(style);
 
        // 导出 Excel 文件
        FileOutputStream fileOut = new FileOutputStream(fileName);
        workbook.write(fileOut);
        fileOut.close();
    }
}
使用该类,我们可以方便地读取和导出 Excel 文件,如下所示:

public static void main(String[] args) throws IOException {
    String fileName = "path/to/excel/file.xlsx";
 
    // 读取 Excel 文件
    ExcelUtils.readExcel(fileName);
 
    // 导出 Excel 文件
    ExcelUtils.writeExcel(fileName);
}

6、结论

本文介绍了如何使用 Java 程序灵活读取和导出 Excel 文件。我们首先介绍了 Apache POI 库,它是 Java 操作 Office 文档的一个强大的工具。然后,我们演示了如何使用 POI 库读取 Excel 文件,并解释了如何根据单元如何设置单元格样式。最后,我们提供了一个完整的示例代码,演示了如何使用 ExcelUtils 类来读取和导出 Excel 文件。

 

Java 程序读取和导出 Excel 文件是一项非常实用的技能,尤其是在需要处理大量数据的项目中。使用 Apache POI 库,我们可以方便地读取和写入 Excel 文件,这可以大大提高我们的工作效率。

 

在实际应用中,我们需要根据具体的需求来选择读取和写入 Excel 文件的方法,以达到最好的效果。

本文出自:https://blog.csdn.net/bairo007/article/details/131818662