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

MySQL数据同步到 Redis 缓存的几种方法

1 Mysql查完数据,再同步写入到Redis中
缺点1:会对接口造成延迟,因为同步写入redis本身就有延迟,并且还要做重试,如果redis写入失败,还需要重试,那就更费时间了。

缺点2:不解耦,如果redis崩了,那直接卡线程了

缺点3:如果人为该数据库,那就没法同步了, 除非再人为删除对应的Redis,但删除Redis这个过程也有个时间差

2 Mysql查完数据,通过发送MQ,在消费者线程去同步Redis
缺点1:多了层MQ,也就是会有很大的概率导致同步延迟问题.

缺点2:要对MQ的可用性做预防

缺点3:如果人为该数据库,那就没法同步了

优点1:可以大幅减少接口的延迟返回的问题

优点2:MQ本身有重试机制,无需人工去写重试代码

优点3:解耦,把查询Mysql和同步Redis完全分离,互不干扰

3 订阅Mysql的Binlog文件(可借助Canal来进行)
CanalServer会伪装成MysqlServer从库,去订阅MysqlServer主库的Binlog文件

Canal启动的时候会配置对应的消息MQ(RabbitMQ, RocketMQ, Kafka), 监听到Binlog文件有变化是,会把变化的sql语句转换成json格式,并作为消息内容发送到MQ中

项目中只要监听对应MQ,就能拿到Binlog改动的内容,Json数据中有明确的操作类型(CURD), 以及对应的数据。把对应数据同步到redis即可

缺点1:canal订阅Binlog的整个操作过程是单线程的,所以面临超高并发的情况下,性能可能不太出色。当然可以部署多个Canal 与 多个消费者,但是要注意消息重复消费问题,做好幂等性校验

优点1:即使人为改数据库,也会监听到,并且也会同步

优点2:异步同步,不会对接口返回有格外延迟

4 延迟双删
在执行修改sql之前,先将redis的数据删除

执行更新sql

延迟一段时间

再次删除redis的数据

// 延迟双删伪代码
deleteRedisCache(key); // 删除redis缓存
updateMysqlSql(obj); // 更新mysql
Thread.sleep(100); // 延迟一段时间
deleteRedisCache(key); // 再次删除该key的缓存

缺点:这个延迟时间不好把控,到底延迟多久,这个很难去评估

扩展: 如果不使用延迟双删,仅仅是delete缓存,然后改mysql数据。只有这两步会出现什么问题呢?

5. 单个请求,单线程没问题,高并发多线程下会出问题

6. 如果Thread1线程要更新数据,此时Thread1线程把redis清理了

7. 此时Thread2线程来了,但Thread1还没有更新mysql完毕

8. Thread2查询redis肯定是null,此时Thread2就要查mysql了,然后再把查到的数据写到缓存

9. 由于Thread1还没来得及修改mysql数据,所以此时Thread2查出来的数据是【旧数据】,Thread2把旧数据又写入Redis 了

10. 此时Thread3线程来了,查询Redis发现有数据,则直接拿缓存数据了,此时【Thread3查出来的是旧数据】,直接带着旧数据返回了,这就是问题所在

11. 而延迟双删的第二次删除作用就是防止Thread2把旧数据又写入了,有了延迟双删,Thread3查询Redis的时候还是null,就会从mysql 去拿最新数据了

12. 所以正常的这个延迟时间,应该是Thread2查缓存到拿mysql数据,到再保存到redis这整个时间,作为Thread1的延迟时间,但是这个Thread2这个过程的时间会受到很多因素影响,因此很难断定究竟会是多久

5 延迟双写

// 延迟双写伪代码
updateMysqlSql(obj); // 更新mysql
addRedis(key); // 再次删除该key的缓存

上述代码缺陷;

高并发下,两条线程同时执行上面代码,并对mysql 修改,且修改内容不通,可能会导致Redis与Mysql数据不一致

T1线程执行完updateMysqlSql,释放了行锁,此时T2线程再执行updateMysqlSql 与 addRedis, 最后T1执行addRedis,这种情况会导致数据库改成了T2线程的数据,但Redis却是T1线程的数据

优化

// 完美延迟双写伪代码
开启事务
updateMysqlSql(obj); // 更新mysql
addRedis(key); // 再次删除该key的缓存
提交事务

上述代码改正:

把两句代码放到一个事务里面,只有T1执行完Mysql 与 Redis的时候,T2才能开始执行,就可以保证数据一致性。推荐使用分布式锁

双写缺点:Mysql 与 Redis是单线程的。性能方面不行,因此不推荐使用

6 总结
推荐使用Canal的方式,进行异步同步。其次是MQ方式

本文出自:https://blog.csdn.net/qq_37284798/article/details/129518953

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

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

【MySQL技术专题】「实战开发系列」一同探索一下数据库的加解密函数开发实战指南之AES系列

MySQL的加解密及压缩函数
许多加密和压缩函数返回结果可能包含任意字节值的字符串。如果要存储这些结果,请使用具有VARBINARY或BLOB二进制字符串数据类型的列。这避免了删除尾随空格或转换字符集可能改变数据值的潜在问题,例如使用非二进制字符串数据类型(CHAR、VARCHAR、TEXT)时可能发生的问题。

MySQL加解密函数
MySQL自带的加解密函数主要有以下:

一些加密函数返回 ASCII 字符字符串:MD5()、PASSWORD()、SHA()、SHA1()、SHA2()。它们的返回值为具有由character_set_connection和collation_connection系统确定的字符集和排序规则的字符串 变量。这是一个非二进制字符串,除非字符集为 。

如果应用程序存储来自返回十六进制字符串的函数(如 MD5() 或 SHA1() 的值) 数字,可以通过以下方式获得更有效的存储和比较 使用 UNHEX() 将十六进制表示形式转换为二进制,并将结果存储在 BINARY(N) 列中。

将十六进制字符串存储在 CHAR 列中的大小损失至少为两倍, 如果值存储在使用该字符集的列中,则最多 4 次(其中每个字符使用 <> 字节)。存储字符串也会导致比较速度变慢 因为值较大且需要采用字符集 将排序规则考虑在内。

ENCODE()、DECODE()

已在5.7.2版本弃用,目前仍可用,但将在后续版本中删除。

DES_ENCRYPT()、DES_DECRYPT()

AES_ENCRYPT()加密与AES_DECRYPT()解密
AES高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。

AES_ENCRYPT和AES_DECRYPT在MySQL中是进行加密了,如果你需要对MySQL某些字段进行加解密的话,使用MySQL的加解密函数可能比程序中处理更方便.

AES_ENCRYPT(‘密码’,‘钥匙’)
AES_DECRYPT(表的字段名字,‘钥匙’)
这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。

严格地说,AES和Rijndael加密法并不完全一样(虽然在实际应用中二者可以互换),因为Rijndael加密法可以支持更大范围的区块和密钥长度:AES的区块长度固定为128 比特,密钥长度则可以是128,192或256比特;而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256比特为上限。包括AES-ECB,AES-CBC,AES-CTR,AES-OFB,AES-CFB

函数参数(MySQL版本小于等于5.7.6)
AES_ENCRYPT(str,key_str),其中str为待加密字符串,key_str为秘钥

AES_DECRYPT(crypt_str,key_str),其中crypt_str为已加密的二进制串,key_str为秘钥

已在5.7.6版本弃用,目前仍可用,但将在后续版本中删除。

AES_ENCRYPT()、AES_DECRYPT()

推荐使用这对加解密函数。aes_encrypt()和aes_decrypt()使用官方的aes(高级加密标准)算法(以前称为“rijndael”)实现数据的加密和解密。

加密后的二进制串长度可以通过下面公式计算:

16 * (trunc(string_length / 16) + 1)

函数参数(MySQL版本大于等于5.7.6)
函数参数
AES_ENCRYPT(str,key_str[,init_vector]),其中str为待加密字符串,key_str为秘钥,其中init_vector根据选择不同的块加密模式为可选项

AES_DECRYPT(crypt_str,key_str[,init_vector]),其中crypt_str为已加密的二进制串,key_str为秘钥,其中init_vector根据选择不同的块加密模式为可选项

st和key_str参数可以是任何长度,init_vector参数不得小于16个字符。可以通过block_encryption_mode参数,控制块加密模式,默认值为:aes-128-ecb。可配置的形式为:aes-keylen–mode。

keylen可配置为128, 192, 256

mode可配置为ECB, CBC, CFB1, CFB8, CFB128, OFB。

下表展示了不同mode是否需要init_vector参数。

默认的ECB模式不需要init_vector参数,用法与5.7.4以前相同。

修改块加密模式:

set block_encryption_mode=’aes-256-cbc’;

查看对应的加解密模式

show variables like ‘block%;

block_encryption_mode
此变量控制的块加密模式,基于块的算法,例如 AES。它会影响 AES_ENCRYPT() 和 AES_DECRYPT() 的加密。

block_encryption_mode需要格式中的值,其中 Keylen是关键位和模式的长度为加密模式。该值不区分大小写。允许的键值为 128、192 和 256. 允许的加密模式取决于 MySQL 是否 使用 OpenSSL 或 yaSSL 编译:aes-keylen-mode。

例如,此语句会导致 AES 加密 使用 256 位密钥长度和 CBC 模式的函数:

SET block_encryption_mode = ‘aes-256-cbc’;

尝试将block_encryption_mode设置为包含不受支持的密钥长度或模式的值不支持SSL库。

AES_ENCRYPT(str,key_str[,init_vector][,kdf_name][,salt][,info | iterations])

AES_ENCRYPT加密 字符串 str 使用键字符串 key_str,并返回二进制文件 包含加密输出的字符串。AES_DECRYPT() 使用密钥字符串key_str解密加密字符串crypt_str,并返回原始 纯文本字符串。如果任一函数参数为 ,则该函数返回 。如果 AES_DECRYPT检测到无效 数据或填充不正确,它将返回 . 但是,AES_DECRYPT() 有可能 返回非值(可能是垃圾) 如果输入数据或键无效。NULLNULLNULLNULL

使用AES_DECRYPT()的语句对于基于语句的复制。

AES_DECRYPT(crypt_str,key_str[,init_vector][,kdf_name][,salt][,info | iterations])

AES_ENCRYPT() 和 AES_DECRYPT() 允许控制 块加密模式。block_encryption_mode系统 变量控制基于块的加密模式 算法。其默认值为 ,表示加密 使用 128 位的密钥长度和 ECB 模式。有关说明 此变量的允许值,请参见第 5.1.7 节 “服务器系统变量”。这 可选init_vector参数为 用于为块加密提供初始化向量 需要它的模式。aes-128-ecb

本文出自:https://blog.csdn.net/l569590478/article/details/132187930

ADO 通过 GetString() 加速脚本

请使用 GetString() 方法来加速您的 ASP 脚本(来代替多行的 Response.Write)。

 

多行 Response.Write

下面的例子演示了在 HTML 表格中显示数据库查询的一种方法:

<html>
<body>

<%
set conn=Server.CreateObject("ADODB.Connection")
conn.Provider="Microsoft.Jet.OLEDB.4.0"
conn.Open "c:/webdata/northwind.mdb"

set rs = Server.CreateObject("ADODB.recordset")
rs.Open "SELECT Companyname, Contactname FROM Customers", conn
%>

<table border="1" width="100%">
<%do until rs.EOF%>
  <tr>
    <td><%Response.Write(rs.fields("Companyname"))%></td>
    <td><%Response.Write(rs.fields("Contactname"))%></td>
  </tr>
<%rs.MoveNext
loop%>
</table>

<%
rs.close
conn.close
set rs = Nothing
set conn = Nothing
%>

</body>
</html>

对于一个大型的查询来说,这样做会增加脚本的处理时间,这是由于服务器需要处理大量的 Response.Write 命令。

 

解决的办法是创建全部字符串,从 <table> 到 </table>,然后将其输出 – 只使用一次 Response.Write。

 

GetString() 方法

GetString() 方法使我们有能力仅使用一次 Response.Write,就可以显示所有的字符串。同时它甚至不需要 do..loop 代码以及条件测试来检查记录集是否处于 EOF。

 

语法

str = rs.GetString(format,rows,coldel,rowdel,nullexpr)

 

如需使用来自记录集的数据创建一个 HTML 表格,我们仅仅需要使用以上参数中的三个(所有的参数都是可选的):

 

coldel – 用作列分隔符的 HTML

rowdel – 用作行分隔符的 HTML

nullexpr – 当列为空时所使用的 HTML

注意:GetString() 方法是 ADO 2.0 的特性。您可从下面的地址下载 ADO 2.0:http://www.microsoft.com/data/download.htm

 

在下面的例子中,我们将使用 GetString() 方法,把记录集存为一个字符串:

 

实例

<html>
<body>
  
<%
set conn=Server.CreateObject("ADODB.Connection")
conn.Provider="Microsoft.Jet.OLEDB.4.0"
conn.Open "c:/webdata/northwind.mdb"
  
set rs = Server.CreateObject("ADODB.recordset")
rs.Open "SELECT Companyname, Contactname FROM Customers", conn
  
str=rs.GetString(,,"</td><td>","</td></tr><tr><td>","&nbsp;")
%>
  
<table border="1" width="100%">
  <tr>
    <td><%Response.Write(str)%></td>
  </tr>
</table>
  
<%
rs.close
conn.close
set rs = Nothing
set conn = Nothing
%>
</body>
</html> 

上面的变量 str 包含着由 SELECT 语句返回的所有列和行的一个字符串。在每列之间会出现 </td><td>,在每行之间会出现 </td></tr><tr><td>。这样,仅使用一次 Response.Write,我们就得到了需要的 HTML。

本文出自:https://blog.csdn.net/unbelievevc/article/details/133854601

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

Python的pandas库来实现将Excel文件转换为JSON格式的操作

1、使用Flask将数据转为JSON数据提供给客户端

如果您想创建一个本地接口来提供商品信息,您可以考虑使用Python以及一些流行的库来实现这个目标。以下是一些步骤和建议:

 

准备数据: 首先,确保您的Excel表包含商品信息,并将其保存为一个常见的数据格式,如CSV文件。这将使数据更容易在Python中处理。

 

安装Python: 如果您还没有安装Python,可以从Python官方网站下载并安装Python的最新版本。

 

选择Web框架: 为了创建本地接口,您可以选择一个Python的Web框架,如Flask或Django。对于初学者来说,Flask可能更容易入手。您可以使用以下命令安装Flask:

pip install Flask

创建Flask应用: 使用Flask,您可以创建一个简单的Web应用程序,将商品信息作为JSON数据提供给客户端。以下是一个简单的示例:

from flask import Flask, jsonify
import pandas as pd

app = Flask(__name__)

# 读取CSV文件
data = pd.read_csv('商品信息.csv')

@app.route('/api/products', methods=['GET'])
def get_products():
    # 将CSV数据转换为JSON格式
    products = data.to_dict(orient='records')
    return jsonify(products)

if __name__ == '__main__':
    app.run(debug=True)

这个示例创建了一个Flask应用,它可以在访问/api/products端点时返回商品信息的JSON数据。

 

运行应用程序: 保存上述代码为一个Python文件(例如app.py),然后在命令行中运行它:

python app.py

这将启动本地Web服务器,并使您的接口在http://127.0.0.1:5000/api/products上可用。

 

测试接口: 使用浏览器或工具如Postman来测试您的接口。访问http://127.0.0.1:5000/api/products应该返回商品信息的JSON数据。

 

这只是一个简单的示例,您可以根据需要扩展和自定义接口。此外,如果您希望接口支持其他操作(如添加、编辑、删除商品信息),您可以在Flask应用中添加相应的路由和处理程序。

 

2、使用pandas库实现Excel文件转换为JSON格式

使用Python的pandas库来实现将Excel文件转换为JSON格式的操作。以下是一个简单的示例代码:

 

首先,确保您已经安装了pandas库。如果没有安装,可以使用以下命令进行安装:

pip install pandas

然后,您可以使用以下代码将Excel文件转换为JSON:

import pandas as pd

读取Excel文件

df = pd.read_excel(‘your_excel_file.xlsx’) # 请将文件名替换为实际的Excel文件名

 

将DataFrame转换为JSON

json_data = df.to_json(orient=‘records’)

 

上述代码中,your_excel_file.xlsx应该替换为实际的Excel文件名。df.to_json(orient=‘records’)将DataFrame对象转换为JSON格式,并以列表的形式返回。

 

最后,您可以在Flask应用中将JSON数据提供给客户端,例如:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/data', methods=['GET'])
def get_data():
    # 这里将之前转换的JSON数据返回给客户端
    return jsonify(json_data)

if __name__ == '__main__':
    app.run(debug=True)

在这个示例中,当访问/api/data端点时,Flask将返回之前转换的JSON数据。

 

请确保将文件名和路由根据您的实际需求进行适当更改。同时,也请注意异常处理和数据清理等方面的工作,以确保您的应用能够稳健运行。

本文出自:https://blog.csdn.net/hh867308122/article/details/132763586

《操作系统》- 线程与进程

一、进程的概念

程序放在磁盘上不叫做进程,只有运行起来之后才叫做进程,进程进程就是进行中的程序!

一次运行就会有一个进程,当再次运行的时候就会产生一个新的进程,浏览器就是这样的,而有的应用程序就不是,当已经在运行的时候再次运行会打开上次运行的程序。我们可以打开应用然后通过任务管理器进行观察就可以发现。

进程是操作系统进行资源分配和调度的一个独立单位(或者说是基本单位)

二、进程的结构和特征

2.1 进程的结构

进程主要有控制块(PCB)、数据段、程序段三部分构成,下图当中把这三块比喻成了一个人,也就是进程。

 

控制块:操作系统就是根据PCB来区分进程的,可以理解为每个进程有一个自己的唯一编号,就好比每个人都有自己的身份证一样,每个进程的PCB都是不重复的。

数据段:每个进程的数据都是不一样的,并且不存在共享,可以理解为就是人的身体

程序段:可以理解为就是磁盘当中存储的应用,以程序员的理解其实就是应用的代码,也就是多个进程可以共享一个程序代码。再通透一点,电脑安装了一个浏览器,却可以打开多个浏览器页面,多个浏览器页面就是多个进程。

PCB的存储信息与分类

2.2 进程的特征

动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的

并发性:多个进程可以并发执行

独立性:独立资源分配,每个进程都有自己的内存区域。

异步性:不用等待别的进程处理完成所以他是异步的、相互独立的、互不干扰。

三、线程的概念

线程可以理解为就是从进程当中分裂出来的一个指令集合。一个进程可以包含多个线程。也可以只存在一条线程,并且线程不能单独存在。线程的资源是由进程进行管理的。

 

我们打开一个应用就是进程,然后应用当中的每一个功能就对应了一个线程。

拿qq举例:打开qq就是一个进程的运行,这个时候在任务管理器当中是可以看得到该进程的,然后打开qq里面的qq空间就是一条线程,然后跟A聊天就又是一个线程,跟B聊天就又是一个线程。

 

 

面试的时候经常会问进程线程的区别:

 

进程是操作系统 资源分配调度 的基本单位

线程是操作系统 运算调度 的最小单位

想不起来的时候就想一下电脑的任务管理器,任务管理器当中显示了进程,并且显示了进程占用的内存CPU等信息。也就是操作系统是给进程分配空间而并不是线程。

 

线程下还有一个纤程(了解即可)

 

纤程:线程是在Windows内核中实现的,操作系统会根据系统的调度算法对线程进行调度。 纤程是在用户模式下实现的,内核对纤程一无所知。 纤程是更轻量级的线程,一个线程可以包含一个或多个纤程。

Java 原生没有提供纤程支持,需要依赖于 quasar-core的库,来创建纤程。感兴趣的可以了解一下,据说是多线程与多纤程计算耗时相比较差距是非常大的,多纤程要比多线程快的多。

 

线程的属性:

四、进程和线程比较

五、线程的实现方式

线程的实现分为两类:用户级线程(User-Level Thread,UTL)和内核级线程(Kernel-Level Thread, KTL)l。内核级线程又称内核支持的线程。

 

在用户空间完成的线程就是用户级线程、在内核空间完成的线程就是内核级线程。这里的完成,包含了线程的创建、执行、和销毁。

 

(1)用户级方式

操作系统提供了线程库,线程库包含了创建线程和销毁线程以及执行线程的接口。操作系统根本不知道线程这个东西,他在执行的时候是以进程装载线程指令来执行的。假如某个线程想要操作内核,这时候会通过陷入指令进行中断,一旦中断其他线程都将阻塞。优点:如果线程不访问内核,只是线程切换,效率比较高。因为所有的线程都在用户空间。

 

 

优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高。

缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行(因为此时只有一个内核级线程)。

(2)内核级方式

内核级线程的线程控制块在内核空间,上面我们提到了进程由控制块(PCB)、数据段、程序段这三部分构成,而线程他也有,只不过线程的控制块是叫做线程控制块(TCB)。内核级方式就是将线程的控制块存放到内核当中,然后处理器直接切换控制块来完成线程的切换。这时候就完成了线程的并行。

 

 

优点:当一个用户级线程被阻塞后,别的用户级线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。

缺点:一个用户进程会占用多个内核级线程,而内核级线程的切换由操作系统内核完成,需要切换到核心态,因此线程管理成本高、开销大。

本质上内核级方式和用户级方式 两者的区别就是线程的控制块到底是在用户空间还是在内核空间。

 

(3)特殊的组合方式

这种模型克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点,是上述两种模型的折中方案。

本文出自:https://blog.csdn.net/weixin_43888891/article/details/131534525

纯后台生成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