一、前言

说到二维码,我相信大家每天都会用到,尤其是在手机支付的场景,使用频率极广。在实际的业务开发过程中,二维码的使用场景开发也会经常出现在我们开发人员的面前,我们应该如何去处理呢,今天就带着大家一起深入的了解一下它的技术实现过程。

二、代码实践

在 Java 生态体系里面,操作二维码的开源项目很多,如 SwetakeQRCode、BarCode4j、Zxing 等等。

今天我们介绍下简单易用的 google 公司的 zxing,zxing 不仅使用方便,而且可以还操作条形码或者二维码等,不仅有 java 版本,还有 Android 版。

开源库地址:

GitHub 开源地址:https://github.com/zxing/zxing
zxing 二进制包下载地址:http://repo1.maven.org/maven2/com/google/zxing
zxing Maven 仓库地址:https://mvnrepository.com/artifact/com.google.zxing

通过 Maven 仓库,我们可以很轻松的将其依赖包添加到自己的项目。

2.1 添加依赖

1
2
3
4
5
6
7
8
9
10
11
        <!-- 如果是非 web 应用则导入 core 包即可,如果是 web 应用,则 core 与 javase 一起导入。-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.0</version>
</dependency>

2.2 生成普通二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* CODE_WIDTH:二维码宽度,单位像素
* CODE_HEIGHT:二维码高度,单位像素
* FRONT_COLOR:二维码前景色,0x000000 表示黑色
* BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色
* 演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白
*/
private static final int CODE_WIDTH = 512;
private static final int CODE_HEIGHT = 512;
private static final int FRONT_COLOR = 0x000000;
private static final int BACKGROUND_COLOR = 0xFFFFFF;
private static MultiFormatWriter mutiWriter = new MultiFormatWriter();

/**
* 生成二维码 并 保存为图片
*
* @param codeContent
*/
public static void createCodeToFile(String codeContent) {
try {
//获取系统目录
String filePathDir = FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath();
//随机生成 png 格式图片
String fileName = new Date().getTime() + ".png";

/**com.google.zxing.EncodeHintType:编码提示类型,枚举类型
* EncodeHintType.CHARACTER_SET:设置字符编码类型
* EncodeHintType.ERROR_CORRECTION:设置误差校正
* ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
* 不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的
* EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近
* */
Map<EncodeHintType, Object> hints = new HashMap();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 1);

/**
* MultiFormatWriter:多格式写入,这是一个工厂类,里面重载了两个 encode 方法,用于写入条形码或二维码
* encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints)
* contents:条形码/二维码内容
* format:编码类型,如 条形码,二维码 等
* width:码的宽度
* height:码的高度
* hints:码内容的编码类型
* BarcodeFormat:枚举该程序包已知的条形码格式,即创建何种码,如 1 维的条形码,2 维的二维码 等
* BitMatrix:位(比特)矩阵或叫2D矩阵,也就是需要的二维码
*/
BitMatrix bitMatrix = mutiWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);

/**java.awt.image.BufferedImage:具有图像数据的可访问缓冲图像,实现了 RenderedImage 接口
* BitMatrix 的 get(int x, int y) 获取比特矩阵内容,指定位置有值,则返回true,将其设置为前景色,否则设置为背景色
* BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
* x:像素位置的横坐标,即列
* y:像素位置的纵坐标,即行
* rgb:像素的值,采用 16 进制,如 0xFFFFFF 白色
*/
BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
for (int x = 0; x < CODE_WIDTH; x++) {
for (int y = 0; y < CODE_HEIGHT; y++) {
bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
}
}

/**javax.imageio.ImageIO java 扩展的图像IO
* write(RenderedImage im,String formatName,File output)
* im:待写入的图像
* formatName:图像写入的格式
* output:写入的图像文件,文件不存在时会自动创建
*
* 即将保存的二维码图片文件*/
File codeImgFile = new File(filePathDir, fileName);
ImageIO.write(bufferedImage, "png", codeImgFile);
System.out.println("二维码图片生成成功:" + codeImgFile.getPath());
} catch (Exception e) {
e.printStackTrace();
}
}

2.3 生成彩色带图标二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    /**
* CODE_WIDTH:二维码宽度,单位像素
* CODE_HEIGHT:二维码高度,单位像素
* FRONT_COLOR:二维码前景色,0x000000 表示黑色
* BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色
* IMAGE_WIDTH:二维码中间图标内框宽度
* IMAGE_HEIGHT: 二维码中间图标内框高度
* 演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白
*/
private static final int CODE_WIDTH = 512;
private static final int CODE_HEIGHT = 512;
private static final int IMAGE_WIDTH = 100;
private static final int IMAGE_HEIGHT = 100;
private static final int IMAGE_HALF_WIDTH = IMAGE_WIDTH / 2;
private static final int FRAME_WIDTH = 2;
private static MultiFormatWriter mutiWriter = new MultiFormatWriter();

/**
* 生成彩色二维码 并 保存为图片
*
* @param content
* @param srcImagePath
*/
public static void createColorCodeToFile(String content, String srcImagePath) {
try {
//获取系统目录
String filePathDir = FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath();
//随机生成 png 格式图片
String fileName = new Date().getTime() + ".png";
BufferedImage scaleImage = scale(srcImagePath, IMAGE_WIDTH, IMAGE_HEIGHT, true);
int[][] srcPixels = new int[IMAGE_WIDTH][IMAGE_HEIGHT];
for (int i = 0; i < scaleImage.getWidth(); i++) {
for (int j = 0; j < scaleImage.getHeight(); j++) {
srcPixels[i][j] = scaleImage.getRGB(i, j);
}
}
Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 1);
// 生成二维码
BitMatrix matrix = mutiWriter.encode(content, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);
// 二维矩阵转为一维像素数组
int halfW = matrix.getWidth() / 2;
int halfH = matrix.getHeight() / 2;
int[] pixels = new int[CODE_WIDTH * CODE_HEIGHT];
for (int y = 0; y < matrix.getHeight(); y++) {
for (int x = 0; x < matrix.getWidth(); x++) {
// 左上角颜色,根据自己需要调整颜色范围和颜色
if (x > 0 && x < 170 && y > 0 && y < 170) {
Color color = new Color(231, 144, 56);
int colorInt = color.getRGB();
pixels[y * CODE_WIDTH + x] = matrix.get(x, y) ? colorInt : 16777215;
}
// 读取图片
else if (x > halfW - IMAGE_HALF_WIDTH
&& x < halfW + IMAGE_HALF_WIDTH
&& y > halfH - IMAGE_HALF_WIDTH
&& y < halfH + IMAGE_HALF_WIDTH) {
pixels[y * CODE_WIDTH + x] = srcPixels[x - halfW + IMAGE_HALF_WIDTH][y - halfH + IMAGE_HALF_WIDTH];
} else if ((x > halfW - IMAGE_HALF_WIDTH - FRAME_WIDTH
&& x < halfW - IMAGE_HALF_WIDTH + FRAME_WIDTH
&& y > halfH - IMAGE_HALF_WIDTH - FRAME_WIDTH && y < halfH + IMAGE_HALF_WIDTH + FRAME_WIDTH)
|| (x > halfW + IMAGE_HALF_WIDTH - FRAME_WIDTH
&& x < halfW + IMAGE_HALF_WIDTH + FRAME_WIDTH
&& y > halfW - IMAGE_HALF_WIDTH - FRAME_WIDTH && y < halfH + IMAGE_HALF_WIDTH + FRAME_WIDTH)
|| (x > halfW - IMAGE_HALF_WIDTH - FRAME_WIDTH
&& x < halfW + IMAGE_HALF_WIDTH + FRAME_WIDTH
&& y > halfH - IMAGE_HALF_WIDTH - FRAME_WIDTH && y < halfH - IMAGE_HALF_WIDTH + FRAME_WIDTH)
|| (x > halfW - IMAGE_HALF_WIDTH - FRAME_WIDTH
&& x < halfW + IMAGE_HALF_WIDTH + FRAME_WIDTH
&& y > halfH + IMAGE_HALF_WIDTH - FRAME_WIDTH && y < halfH + IMAGE_HALF_WIDTH + FRAME_WIDTH)) {
pixels[y * CODE_WIDTH + x] = 0xfffffff;
// 在图片四周形成边框
} else {
// 二维码颜色
int num1 = (int) (50 - (50.0 - 13.0) / matrix.getHeight() * (y + 1));
int num2 = (int) (165 - (165.0 - 72.0) / matrix.getHeight() * (y + 1));
int num3 = (int) (162 - (162.0 - 107.0) / matrix.getHeight() * (y + 1));
Color color = new Color(num1, num2, num3);
int colorInt = color.getRGB();
// 此处可以修改二维码的颜色,可以分别制定二维码和背景的颜色;
pixels[y * CODE_WIDTH + x] = matrix.get(x, y) ? colorInt : 16777215;
// 0x000000:0xffffff
}
}
}
BufferedImage image = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_RGB);
image.getRaster().setDataElements(0, 0, CODE_WIDTH, CODE_HEIGHT, pixels);
File codeImgFile = new File(filePathDir, fileName);
ImageIO.write(image, "jpg", codeImgFile);
System.out.println("二维码图片生成成功:" + codeImgFile.getPath());
} catch (IOException e) {
e.printStackTrace();
} catch (WriterException e) {
e.printStackTrace();
}
}


private static BufferedImage scale(String srcImageFile, int height, int width, boolean hasFiller) throws IOException {
double ratio = 0.0; // 缩放比例
File file = new File(srcImageFile);
BufferedImage srcImage = ImageIO.read(file);
Image destImage = srcImage.getScaledInstance(width, height,
BufferedImage.SCALE_SMOOTH);
// 计算比例
if ((srcImage.getHeight() > height) || (srcImage.getWidth() > width)) {
if (srcImage.getHeight() > srcImage.getWidth()) {
ratio = (new Integer(height)).doubleValue()
/ srcImage.getHeight();
} else {
ratio = (new Integer(width)).doubleValue()
/ srcImage.getWidth();
}
AffineTransformOp op = new AffineTransformOp(
AffineTransform.getScaleInstance(ratio, ratio), null);
destImage = op.filter(srcImage, null);
}
if (hasFiller) {
// 补白
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
Graphics2D graphic = image.createGraphics();
graphic.setColor(Color.white);
graphic.fillRect(0, 0, width, height);
if (width == destImage.getWidth(null)) {
graphic.drawImage(destImage, 0,
(height - destImage.getHeight(null)) / 2,
destImage.getWidth(null), destImage.getHeight(null),
Color.white, null);
} else {
graphic.drawImage(destImage,
(width - destImage.getWidth(null)) / 2, 0,
destImage.getWidth(null), destImage.getHeight(null),
Color.white, null);
graphic.dispose();
destImage = image;
}
}
return (BufferedImage) destImage;
}

2.4 条形码生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    /**
* CODE_WIDTH:条形码宽度,单位像素
* IMAGE_HEIGHT: 条形码高度
* 演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白
*/
private static final int CODE_WIDTH = 512;
private static final int IMAGE_HEIGHT = 100;
private static MultiFormatWriter mutiWriter = new MultiFormatWriter();

// 生成条形码的方法
public static void generateBarcode(String data) {
//获取系统目录
String filePathDir = FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath();
//随机生成 png 格式图片
String fileName = new Date().getTime() + ".png";
try {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 设置字符编码

BitMatrix bitMatrix = mutiWriter.encode(data, BarcodeFormat.CODE_128, CODE_WIDTH, IMAGE_HEIGHT, hints);

// 创建BufferedImage对象来表示条形码
BufferedImage image = new BufferedImage(CODE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < CODE_WIDTH; x++) {
for (int y = 0; y < IMAGE_HEIGHT; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0 : 0xFFFFFF); // 生成黑色条和白色背景的条形码
}
}

// 将条形码保存到文件
File barcodeFile = new File(filePathDir,fileName);
ImageIO.write(image, "png", barcodeFile);

System.out.println("条形码已生成并保存到: " + barcodeFile.getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
}
}

2.4 二维码读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 解析二维码内容(文件)
* @param file
* @return
* @throws IOException
*/
public static String parseQRCodeByFile(File file) throws IOException {
BufferedImage bufferedImage = ImageIO.read(file);
return parseQRCode(bufferedImage);
}

/**
* 解析二维码内容(网络链接)
* @param url
* @return
* @throws IOException
*/
public static String parseQRCodeByUrl(URL url) throws IOException {
BufferedImage bufferedImage = ImageIO.read(url);
return parseQRCode(bufferedImage);
}

private static String parseQRCode(BufferedImage bufferedImage){
try {
/**
* com.google.zxing.client.j2se.BufferedImageLuminanceSource:缓冲图像亮度源
* 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源
* 关键就是下面这几句:HybridBinarizer 用于读取二维码图像数据,BinaryBitmap 二进制位图
*/
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");

/**
* 如果图片不是二维码图片,则 decode 抛异常:com.google.zxing.NotFoundException
* MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵
* MultiFormatReader 的 decode 用于读取二进制位图数据
*/
Result result = new MultiFormatReader().decode(bitmap, hints);
return result.getText();
} catch (Exception e) {
e.printStackTrace();
System.out.println("-----解析二维码内容失败-----");
}
return null;
}

2.5 测试

2.5.1 生成

1
2
3
4
5
6
7
    public static void main(String[] args) {
String codeContent1 = "Hello World";
createCodeToFile(codeContent1);
String codeContent2 = "https://www.baidu.com/";
createCodeToFile(codeContent2);
createColorCodeToFile("http://www.baidu.com/", "C:\\Users\\wno704\\Desktop\\favicon.jpg");
}

结果如下:

二维码图片生成成功:C:\Users\wno704\Desktop\1634476878102.png
二维码图片生成成功:C:\Users\wno704\Desktop\1634476878245.png
二维码图片生成成功:C:\Users\wno704\Desktop\1634476878284.png

具体生成的文件:

2.5.2 读取

读取上面生成的二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws IOException {
File localFile = new File("C:\\Users\\wno704\\Desktop\\1634476878102.png");
String content1 = parseQRCodeByFile(localFile);
System.out.println(localFile + " 二维码内容:" + content1);

localFile = new File("C:\\Users\\wno704\\Desktop\\1634476878245.png");
content1 = parseQRCodeByFile(localFile);
System.out.println(localFile + " 二维码内容:" + content1);

localFile = new File("C:\\Users\\wno704\\Desktop\\1634476878284.png");
content1 = parseQRCodeByFile(localFile);
System.out.println(localFile + " 二维码内容:" + content1);

URL url = new URL("http://cdn.pzblog.cn/1951b6c4b40fd81630903bf6f7037156.png");
String content2 = parseQRCodeByUrl(url);
System.out.println(url + " 二维码内容:" + content2);
}

执行结果:

C:\Users\wno704\Desktop\1634476878102.png 二维码内容:Hello World
C:\Users\wno704\Desktop\1634476878245.png 二维码内容:https://wno704.top
C:\Users\wno704\Desktop\1634476878284.png 二维码内容:https://wno704.top
http://cdn.pzblog.cn/1951b6c4b40fd81630903bf6f7037156.png 二维码内容:https://www.baidu.com/

2.6 web二维码交互

在实际的项目开发过程中,很多时候二维码都是根据参数实时输出到网页上进行显示的,它的实现原理类似验证码,例如下图,它们都是后台先生成内存图像BufferedImage,然后使用ImageIO.write写出来。

在线生成二维码的功能,其实也类似于此!

2.6.1 前端关键代码

1
<img src="http://xxxx/qrCode" alt="验证码,点击刷新!" onclick="this.src=this.src+'?temp='+Math.random();" class="content-code fl-r" />

2.6.2 后端关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class SystemController {

@GetMapping("qrCode")
public void getQRCode(HttpServletResponse response) {
String content = "Hello World";
try {
/**
* 调用工具类生成二维码并输出到输出流中
*/
QRCodeUtil.createCodeToOutputStream(content, response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
}

其中createCodeToOutputStream方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 生成二维码 并 写入流
*
* @param codeContent
*/
public static void createCodeToOutputStream(String codeContent, OutputStream outputStream) {
try {

/**com.google.zxing.EncodeHintType:编码提示类型,枚举类型
* EncodeHintType.CHARACTER_SET:设置字符编码类型
* EncodeHintType.ERROR_CORRECTION:设置误差校正
* ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
* 不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的
* EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近
* */
Map<EncodeHintType, Object> hints = new HashMap();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 1);

/**
* MultiFormatWriter:多格式写入,这是一个工厂类,里面重载了两个 encode 方法,用于写入条形码或二维码
* encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints)
* contents:条形码/二维码内容
* format:编码类型,如 条形码,二维码 等
* width:码的宽度
* height:码的高度
* hints:码内容的编码类型
* BarcodeFormat:枚举该程序包已知的条形码格式,即创建何种码,如 1 维的条形码,2 维的二维码 等
* BitMatrix:位(比特)矩阵或叫2D矩阵,也就是需要的二维码
*/
BitMatrix bitMatrix = mutiWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);

/**java.awt.image.BufferedImage:具有图像数据的可访问缓冲图像,实现了 RenderedImage 接口
* BitMatrix 的 get(int x, int y) 获取比特矩阵内容,指定位置有值,则返回true,将其设置为前景色,否则设置为背景色
* BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
* x:像素位置的横坐标,即列
* y:像素位置的纵坐标,即行
* rgb:像素的值,采用 16 进制,如 0xFFFFFF 白色
*/
BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
for (int x = 0; x < CODE_WIDTH; x++) {
for (int y = 0; y < CODE_HEIGHT; y++) {
bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
}
}

ImageIO.write(bufferedImage, "png", outputStream);
System.out.println("二维码图片生成成功");
} catch (Exception e) {
e.printStackTrace();
}
}

这种方式,如果是单体应用,其实没太大问题,在微服务开发的环境下有局限性。

因此我们还有另外一种玩法,那就是将生成的图片流转成base64的格式,然后返回给前端进行展示。

关键代码改造过程如下:

1
2
3
4
5
//定义字节输出流,将bufferedImage写入
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", out);
//将输出流转换成base64
String str64 = Base64.getEncoder().encodeToString(out.toByteArray());

最后,把base64内容以json的形式返回给前端,进行展示!