很久没来写文章了,自己忙又懒。最近实现了一个需求,有点意思,就想起来记录一下。
业务需求:生成一个二维码,旁边还要加点解释说明什么的,(类似)最终效果如下
一.实现思路
1.二维码生成:使用hutool提供的google二维码生成工具
2.背景图(那个纯白色背景)+文字:使用java自带的Graphics绘制工具
3.批量下载:选择使用java自带的ZipOutputStream压缩流工具
4.性能问题:全部图片的操作都在内存上操作,性能不错,但耗内存
二.实现步骤(编码)
随便拿个springBoot的工程来写个demo
1.添加用到的依赖(pom.xml):
<!-- lombok工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<!-- google工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<!-- 生成二维码所需 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<!-- hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.15</version>
</dependency>
2.编写两个工具类
①.QrCodeUtils.java,图片相关操作以及文件下载工具类(当然,可以再进行分割,这里是demo,就随便封装一下)
package com.cloud.sbjm.common;
import com.cloud.sbjm.onput.vo.DownloadFile;
import com.cloud.sbjm.onput.vo.MessageQrCodeVo;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import cn.hutool.extra.qrcode.QrCodeUtil;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author :ouyangzhicheng
* @date :Created in 2019-9-17 14:54
* @description:图片工具类
* @version: 1.0.0
*/
@Component
@Slf4j
public class QrCodeUtils {
private static final String MESSAGE_NUM = "XX编码";
private static final String MESSAGE_CATEGORY = "XX类别";
private static final String MESSAGE_QRCODE = "XX二维码";
private static final String FONT_TYPE = "宋体";
private static String MESSAGE_QRCODE_URL;
private static final String DISPLAYNAME = "XX二维码.png";
@Value("${message-qrcode-url}")
public void setMESSAGE_QRCODE_URL(String MESSAGE_QRCODE_URL) {
this.MESSAGE_QRCODE_URL = MESSAGE_QRCODE_URL;
}
/**
* 生成资产管理二维码图片
*/
public static InputStream generateAssetsQrCodeImage(MessageQrCodeVo messageQrCodeVo) throws IOException{
// 获取图片的缓冲区,也就是所谓的画布
BufferedImage bufferedImage = new BufferedImage(600, 350, BufferedImage.TYPE_INT_RGB);
//获取画笔,画笔用于在画布上进行绘制
Graphics paint = bufferedImage.getGraphics();
//设置画笔的颜色
paint.setColor(Color.white);
//绘制画布的背景色
paint.fillRect(0, 0, 600, 350);
//生成二维码图片字节流
byte[] qrCodeFile = QrCodeUtil.generatePng(MESSAGE_QRCODE_URL+"?id="+messageQrCodeVo.getId(), 180, 180);
return overlapImage(bufferedImage,qrCodeFile,messageQrCodeVo);
}
/**
* 图片重叠
* @param bufferedImage
* @param qrCodeFile
* @param messageQrCodeVo
* @return
*/
public static InputStream overlapImage(BufferedImage bufferedImage, byte[] qrCodeFile, MessageQrCodeVo messageQrCodeVo) throws IOException {
ByteArrayOutputStream os = null;
InputStream qrCodeInputStream =null;
try {
BufferedImage qrCode = ImageIO.read(new ByteArrayInputStream(qrCodeFile));
//在背景图片中添加入需要写入的信息
Graphics2D g = bufferedImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g.setColor(Color.black);
g.setFont(new Font(FONT_TYPE,Font.BOLD,25));
g.drawString(messageQrCodeVo.getName(),240 ,120);
g.setColor(Color.gray);
g.setFont(new Font(FONT_TYPE,Font.BOLD,20));
g.drawString(MESSAGE_NUM,240 ,160);
g.drawString(MESSAGE_CATEGORY,240 ,200);
g.drawString(MESSAGE_QRCODE,70 ,270);
g.setColor(Color.black);
g.drawString(messageQrCodeVo.getNumber(),345 ,160);
g.drawString(messageQrCodeVo.getTypeName(),345 ,200);
//在背景图片上添加二维码图片
g.drawImage(qrCode, 30, 65, qrCode.getWidth(), qrCode.getHeight(), null);
g.dispose();
os = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", os);
qrCodeInputStream = new ByteArrayInputStream(os.toByteArray());
}catch (Exception e){
log.warn("生成二维码出错:"+e.getMessage());
}finally {
if(os!=null){
os.close();
}
if(qrCodeInputStream!=null){
qrCodeInputStream.close();
}
}
return qrCodeInputStream;
}
/**
* 资产二维码下载(包含文字信息)
*/
public static void messageQrCodeDownload(HttpServletResponse response, List<MessageQrCodeVo> messageQrCodeVos) throws IOException {
InputStream qrCodeInputStream = null;
OutputStream outputStream = null;
try (Closer closer = Closer.create()) {
//单个二维码导出
if(messageQrCodeVos.size() == 1){
qrCodeInputStream = generateAssetsQrCodeImage(messageQrCodeVos.get(0));
closer.register(qrCodeInputStream);
// String charset = StandardCharsets.UTF_8.displayName();
String charset = "GB2312";
response.setCharacterEncoding(charset);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString());
String filename = new String(DISPLAYNAME.getBytes(charset), StandardCharsets.ISO_8859_1);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
outputStream = closer.register(response.getOutputStream());
ByteStreams.copy(qrCodeInputStream, outputStream);
outputStream.flush();
}
//批量二维码导出
if(messageQrCodeVos.size()>1){
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=QrCodes.zip");
List<DownloadFile> downloadFiles =new ArrayList<>();
int i = 0;
for(MessageQrCodeVo messageQrCodeVo:messageQrCodeVos){
i++;
DownloadFile downloadFile = new DownloadFile();
downloadFile.setFileName("XX二维码"+i+".png");
downloadFile.setInputStream(generateAssetsQrCodeImage(messageQrCodeVo));
downloadFiles.add(downloadFile);
}
ZipUtil.toZip3(downloadFiles, response.getOutputStream());
}
}catch (Exception e){
log.warn("二维码下载出错:"+e.getMessage());
}finally {
if(qrCodeInputStream!=null){
qrCodeInputStream.close();
}
if(outputStream!=null){
outputStream.close();
}
}
}
/**
* 普通文件下载
*/
public static void imageDownload(HttpServletResponse response, InputStream inputStream, String fileName) throws IOException {
OutputStream outputStream = null;
try (Closer closer = Closer.create()) {
closer.register(inputStream);
String charset = "GB2312";
response.setCharacterEncoding(charset);
response.setContentType(MediaType.IMAGE_PNG.toString());
String filename = new String(fileName.getBytes(charset), StandardCharsets.ISO_8859_1);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
outputStream = closer.register(response.getOutputStream());
ByteStreams.copy(inputStream, outputStream);
outputStream.flush();
}catch (Exception e){
log.warn("图片下载出错:"+e.getMessage());
}finally {
if(inputStream!=null){
inputStream.close();
}
if(outputStream!=null){
outputStream.close();
}
}
}
}
②.ZipUtil.java,压缩文件操作工具类
package com.cloud.sbjm.common;
import com.cloud.sbjm.onput.vo.DownloadFile;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* @author :ouyangzhicheng
* @date :Created in 2019-9-24 16:03
* @description:压缩文件处理
* @version: 1.0.0
*/
public class ZipUtil {
/**
* 解压缩zip包
*
* @param zipFilePath zip文件的全路径
* @param unzipFilePath 解压后的文件保存的路径
* @param includeZipFileName 解压后的文件保存的路径是否包含压缩文件的文件名。true-包含;false-不包含,目前只支持true
* @param charset The {@linkplain Charset charset} to be
* used to decode the ZIP entry name and comment (ignored if
* the <a href="package-summary.html#lang_encoding"> language
* encoding bit</a> of the ZIP entry's general purpose bit
* flag is set
*/
public static List<String> unZip(String zipFilePath, String unzipFilePath, boolean includeZipFileName, Charset charset) throws IOException {
List<String> filePaths = new ArrayList<>(32);
if (Strings.isNullOrEmpty(zipFilePath)) {
return filePaths;
}
File zipFile = new File(zipFilePath);
// 如果解压后的文件保存路径包含压缩文件的文件名,则追加该文件名到解压路径
String fileName = zipFile.getName();
int pos;
if (includeZipFileName) {
pos = fileName.lastIndexOf(".");
if (pos > 0) {
fileName = fileName.substring(0, pos);
}
unzipFilePath = unzipFilePath + File.separator + fileName;
}
// 开始解压
try (ZipFile zip = new ZipFile(zipFile, charset); Closer closer = Closer.create()) {
for (Enumeration<? extends ZipEntry> zipEntries = zip.entries(); zipEntries.hasMoreElements(); ) {
ZipEntry zipEntry = zipEntries.nextElement();
if (zipEntry.isDirectory()) {
continue;
}
String zipEntryName = zipEntry.getName();
String outPath = unzipFilePath + File.separator + zipEntryName;
File outFile = new File(outPath);
//目录不存在就创建吧
com.google.common.io.Files.createParentDirs(outFile);
BufferedInputStream bufferedIn = closer.register(new BufferedInputStream(zip.getInputStream(zipEntry)));
BufferedOutputStream bufferedOut = closer.register(new BufferedOutputStream(new FileOutputStream(outFile)));
ByteStreams.copy(bufferedIn, bufferedOut);
filePaths.add(outPath.replace("\\", "/"));
}
}
return filePaths;
}
/**
* 解压,返回所有文件的路径
*
* @param zipFilePath 压缩包的路径
* @param unZipFilePath 解压之后的目录
* @param includeZipFileName 解压之后是否用压缩包名作为父目录
* @return 返回文件的路径
* @throws IOException I/O异常
*/
public static List<String> unZip(String zipFilePath, String unZipFilePath, boolean includeZipFileName) throws IOException {
Charset defaultCharset = Charset.forName("GBK");
return unZip(zipFilePath, unZipFilePath, includeZipFileName, defaultCharset);
}
private static final int BUFFER_SIZE = 2 * 1024;
/**
* 压缩成ZIP 方法1
*
* @param srcDir 压缩文件夹路径
* @param out 压缩文件输出流
* @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
* @throws RuntimeException 压缩失败会抛出运行时异常
*/
public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure) throws RuntimeException {
long start = System.currentTimeMillis();
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(out);
File sourceFile = new File(srcDir);
compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
long end = System.currentTimeMillis();
System.out.println("压缩完成,耗时:" + (end - start) + " ms");
} catch (Exception e) {
throw new RuntimeException("zip error from ZipUtils", e);
} finally {
if (zos != null) {
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 压缩成ZIP 方法2
*
* @param srcFiles 需要压缩的文件列表
* @param out 压缩文件输出流
* @throws RuntimeException 压缩失败会抛出运行时异常
*/
public static void toZip(List<File> srcFiles, List<String> fileNames, OutputStream out) throws RuntimeException {
long start = System.currentTimeMillis();
ZipOutputStream zos = null;
int i = 0;
try {
zos = new ZipOutputStream(out);
FileInputStream in=null;
for (File srcFile : srcFiles) {
byte[] buf = new byte[BUFFER_SIZE];
zos.putNextEntry(new ZipEntry(fileNames.get(i)));
i++;
int len;
try{
in = new FileInputStream(srcFile);
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
}
finally {
zos.closeEntry();
if(in!=null){
in.close();
}
}
}
long end = System.currentTimeMillis();
System.out.println("压缩完成,耗时:" + (end - start) + " ms");
} catch (Exception e) {
throw new RuntimeException("zip error from ZipUtils", e);
} finally {
if (zos != null) {
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 压缩成ZIP 方法3
* @param downloadFiles 需要压缩的文件列表
* @param out 压缩文件输出流
* @throws RuntimeException 压缩失败会抛出运行时异常
*/
public static void toZip3(List<DownloadFile> downloadFiles, OutputStream out) throws RuntimeException {
long start = System.currentTimeMillis();
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(out);
InputStream in=null;
for (DownloadFile downloadFile : downloadFiles) {
byte[] buf = new byte[BUFFER_SIZE];
zos.putNextEntry(new ZipEntry(downloadFile.getFileName()));
int len;
try{
in = downloadFile.getInputStream();
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
}finally {
zos.closeEntry();
in.close();
}
}
long end = System.currentTimeMillis();
System.out.println("压缩完成,耗时:" + (end - start) + " ms");
} catch (Exception e) {
throw new RuntimeException("zip error from ZipUtils", e);
} finally {
if (zos != null) {
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 递归压缩方法
*
* @param sourceFile 源文件
* @param zos zip输出流
* @param name 压缩后的名称
* @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
* @throws Exception
*/
private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean KeepDirStructure) throws Exception {
byte[] buf = new byte[BUFFER_SIZE];
FileInputStream in=null;
try {
if (sourceFile.isFile()) {
// 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
zos.putNextEntry(new ZipEntry(name));
// copy文件到zip输出流中
int len;
in = new FileInputStream(sourceFile);
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
// Complete the entry
zos.closeEntry();
in.close();
} else {
File[] listFiles = sourceFile.listFiles();
if (listFiles == null || listFiles.length == 0) {
// 需要保留原来的文件结构时,需要对空文件夹进行处理
if (KeepDirStructure) {
// 空文件夹的处理
zos.putNextEntry(new ZipEntry(name + "/"));
// 没有文件,不需要文件的copy
zos.closeEntry();
}
} else {
for (File file : listFiles) {
// 判断是否需要保留原来的文件结构
if (KeepDirStructure) {
// 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
// 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
} else {
compress(file, zos, file.getName(), KeepDirStructure);
}
}
}
}
}finally {
if(in !=null){
in.close();
}
}
}
}
以上2个工具写好了,就完成了大部分的流程了。
3.编写Controller、Service,将业务模拟出来
①FileController.java
package com.cloud.sbjm.boot;
import com.cloud.sbjm.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletResponse;
/**
* @author :ouyangzhicheng
* @date :Created in 2019-10-10 9:30
* @description:文件处理
* @version: 1.0.0
*/
@Controller
@RequestMapping(value = "/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 生成简单二维码(单个)
* @author: oyzc
* @date: 2019-09-10
*/
@GetMapping(value = "/getQrCode")
public void getQrCode(HttpServletResponse httpResponse, String id){
fileService.getQrCode(httpResponse,id);
}
/**
* 导出包含其他文字信息二维码(单个/批量)
* @author: oyzc
* @date: 2019-09-10
*/
@GetMapping(value = "/generateMessageQrCode")
public void generateMessageQrCode(HttpServletResponse httpResponse, String[] ids){
fileService.generateMessageQrCode(httpResponse,ids);
}
}
②.FileService.java
package com.cloud.sbjm.service;
import javax.servlet.http.HttpServletResponse; /**
* @author :ouyangzhicheng
* @date :Created in 2019-10-10 9:32
* @description:文件处理实现类
* @version: 1.0.0
*/
public interface FileService {
/**
* 导出包含其他文字信息二维码(单个/批量)
* @author: oyzc
* @date: 2019-09-10
*/
void generateMessageQrCode(HttpServletResponse httpResponse, String[] ids);
/**
* 生成简单二维码(单个)
* @author: oyzc
* @date: 2019-09-10
*/
void getQrCode(HttpServletResponse httpResponse, String id);
}
③.FileServiceImpl.java
package com.cloud.sbjm.service.Imp;
import cn.hutool.extra.qrcode.QrCodeUtil;
import com.cloud.sbjm.common.QrCodeUtils;
import com.cloud.sbjm.onput.vo.MessageQrCodeVo;
import com.cloud.sbjm.service.FileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author :ouyangzhicheng
* @date :Created in 2019-10-10 9:33
* @description:文件处理实现类
* @version: 1.0.0
*/
@Slf4j
@Service
public class FileServiceImpl implements FileService{
/**
* 导出包含其他文字信息二维码(单个/批量)
* @author: oyzc
* @date: 2019-09-10
*/
@Value("${message-qrcode-url}")
private String MESSAGE_QRCODE_URL;
@Override
public void generateMessageQrCode(HttpServletResponse httpResponse, String[] ids) {
if(ids == null){
return;
}
//测试模拟查询数据库(根据业务需求改动)
List<MessageQrCodeVo> messageQrCodeVoList = new ArrayList<>();
for(String id:ids){
MessageQrCodeVo messageQrCodeVo = new MessageQrCodeVo();
messageQrCodeVo.setId(id);
messageQrCodeVo.setName("XXXXX");
messageQrCodeVo.setNumber("XXXXXXXXXXX");
messageQrCodeVo.setTypeName("XXXXXXXXXXXXXXX");
messageQrCodeVoList.add(messageQrCodeVo);
}
try {
QrCodeUtils.messageQrCodeDownload(httpResponse,messageQrCodeVoList);
} catch (IOException e) {
log.warn("生成二维码出错:"+e.getMessage());
}
}
/**
* 生成简单二维码(单个)
* @author: oyzc
* @date: 2019-09-10
*/
@Override
public void getQrCode(HttpServletResponse httpResponse, String id) {
//生成二维码图片字节流
byte[] qrCodeFile = QrCodeUtil.generatePng(MESSAGE_QRCODE_URL+"?id="+id, 180, 180);
try {
QrCodeUtils.imageDownload(httpResponse,new ByteArrayInputStream(qrCodeFile),"qrcode.png");
} catch (IOException e) {
log.warn("获取二维码出错:"+e.getMessage());
}
}
}
以上基本模拟完一个简单的业务流程,因为是demo,没有涉及dao层,可根据自身业务进行改造
4.相关的vo类和配置文件也贴一下
①.DownloadFile.java
package com.cloud.sbjm.onput.vo;
import lombok.Data;
import java.io.InputStream;
/**
* @author :ouyangzhicheng
* @date :Created in 2019-9-24 16:14
* @description:下载文件列表(流文件)
* @version: 1.0.0
*/
@Data
public class DownloadFile {
/**
* 文件名称
*/
private String fileName;
/**
* 文件输入流
*/
private InputStream inputStream;
}
②.MessageQrCodeVo.java
package com.cloud.sbjm.onput.vo;
import lombok.Data;
/**
* @author :ouyangzhicheng
* @date :Created in 2019-9-17 19:18
* @description:二维码所需参数实体
* @version: 1.0.0
*/
@Data
public class MessageQrCodeVo {
/**
* Id
*/
private String id;
/**
* 名称
*/
private String name;
/**
* 编码
*/
private String number;
/**
* 类别
*/
private String typeName;
}
③.application.yml配置文件,添加一个参数,二维码的链接
message-qrcode-url = www.baidu.com
自此,把所有涉及的代码都贴出来了,各位有需要可以根据自身业务进行改造
三.测试那些事
为了测试,我过滤掉了所有的校验,直接通过浏览器发送请求来进行测试
demo是根据参数ids所传的个数来决定是单个下载还是批量下载
1.单个下载
图片打开后:
2.批量下载(压缩包)
打开文件后:
自此,功能测试通过。
如有错漏,请大伙指正,谢谢。