在 二维码初体验 com.google.zxing 实现 我们实现了二维码的生成,但是大部分情况下,二维码的相关功能是作为API接口来提供服务的。
我们下面便演示在springboot、Knife4j下封装api接口来实现二维码生成功能。
如何使用下面的备份文件恢复成原始的项目代码,请移步查阅:神奇代码恢复工具
dog.jpg
//goto pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fly</groupId>
<artifactId>qrcode-web</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 异步日志,需要加入disruptor依赖 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
</plugins>
</build>
</project>
//goto src\main\java\com\fly\core\config\Knife4jConfig.java
package com.fly.core.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import io.swagger.annotations.ApiOperation;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
/**
* knife4j
*
* @author jack
*/
@EnableKnife4j
@Configuration
@EnableSwagger2WebMvc
@Import(BeanValidatorPluginsConfiguration.class)
public class Knife4jConfig
{
@Value("${knife4j.enable: true}")
private boolean enable;
@Bean
Docket defaultApi()
{
return new Docket(DocumentationType.SWAGGER_2).enable(enable).apiInfo(apiInfo()).groupName("0.1版").select().apis(RequestHandlerSelectors.basePackage("com.fly.qrcode.api"))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) // 同时满足apis多个条件
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo()
{
return new ApiInfoBuilder().title("数据接口API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
}
}
//goto src\main\java\com\fly\core\utils\SpringContextUtils.java
package com.fly.core.utils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.ServletContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import lombok.extern.slf4j.Slf4j;
/**
* Spring Context 工具类
*
* @author 00fly
*
*/
@Slf4j
@Component
public class SpringContextUtils implements ApplicationContextAware
{
private static ApplicationContext applicationContext;
/**
* web服务器基准URL
*/
private static String SERVER_BASE_URL = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException
{
log.info("###### execute setApplicationContext ######");
SpringContextUtils.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext()
{
return applicationContext;
}
public static <T> T getBean(Class<T> clazz)
{
Assert.notNull(applicationContext, "applicationContext is null");
return applicationContext.getBean(clazz);
}
/**
* execute @PostConstruct May be SpringContextUtils not inited, throw NullPointerException
*
* @return
*/
public static String getActiveProfile()
{
Assert.notNull(applicationContext, "applicationContext is null");
String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
return StringUtils.join(profiles, ",");
}
/**
* can use in @PostConstruct
*
* @param context
* @return
*/
public static String getActiveProfile(ApplicationContext context)
{
Assert.notNull(context, "context is null");
String[] profiles = context.getEnvironment().getActiveProfiles();
return StringUtils.join(profiles, ",");
}
/**
* get web服务基准地址,一般为 http://${ip}:${port}/${contentPath}
*
* @return
* @throws UnknownHostException
* @see [类、类#方法、类#成员]
*/
public static String getServerBaseURL()
throws UnknownHostException
{
if (SERVER_BASE_URL == null)
{
ServletContext servletContext = getBean(ServletContext.class);
Assert.notNull(servletContext, "servletContext is null");
String ip = InetAddress.getLocalHost().getHostAddress();
SERVER_BASE_URL = "http://" + ip + ":" + getProperty("server.port") + servletContext.getContextPath();
}
return SERVER_BASE_URL;
}
/**
* getProperty
*
* @param key eg:server.port
* @return
* @see [类、类#方法、类#成员]
*/
public static String getProperty(String key)
{
return applicationContext.getEnvironment().getProperty(key, "");
}
}
//goto src\main\java\com\fly\qrcode\api\ApiController.java
package com.fly.qrcode.api;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import com.fly.qrcode.qr.QRCodeUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
/**
*
* QR
*
* @author 00fly
* @version [版本号, 2021年4月14日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Controller
@Api(tags = "二维码API接口")
public class ApiController
{
@ApiOperation("生成二维码")
@ApiImplicitParam(name = "content", value = "二维码文本", required = true, example = "乡愁是一棵没有年轮的树,永不老去")
@PostMapping(value = "/create", produces = MediaType.IMAGE_JPEG_VALUE)
public void index(String content, HttpServletResponse response)
throws Exception
{
if (StringUtils.isNotBlank(content))
{
Resource resource = new ClassPathResource("qrCode/dog.jpg");
URL imgURL = resource.getURL();
BufferedImage image = QRCodeUtil.createImage(content, imgURL, true);
// 输出图象到页面
ImageIO.write(image, "JPEG", response.getOutputStream());
}
}
}
//goto src\main\java\com\fly\qrcode\qr\BufferedImageLuminanceSource.java
package com.fly.qrcode.qr;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import com.google.zxing.LuminanceSource;
public class BufferedImageLuminanceSource extends LuminanceSource
{
private BufferedImage image;
private int left;
private int top;
public BufferedImageLuminanceSource(BufferedImage image)
{
this(image, 0, 0, image.getWidth(), image.getHeight());
}
public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height)
{
super(width, height);
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
if (left + width > sourceWidth || top + height > sourceHeight)
{
throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
}
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
if ((image.getRGB(x, y) & 0xFF000000) == 0)
{
image.setRGB(x, y, 0xFFFFFFFF);
}
}
}
this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
this.image.getGraphics().drawImage(image, 0, 0, null);
this.left = left;
this.top = top;
}
@Override
public byte[] getRow(int y, byte[] row)
{
if (y < 0 || y >= getHeight())
{
throw new IllegalArgumentException("Requested row is outside the image: " + y);
}
int width = getWidth();
if (row == null || row.length < width)
{
row = new byte[width];
}
image.getRaster().getDataElements(left, top + y, width, 1, row);
return row;
}
@Override
public byte[] getMatrix()
{
int width = getWidth();
int height = getHeight();
int area = width * height;
byte[] matrix = new byte[area];
image.getRaster().getDataElements(left, top, width, height, matrix);
return matrix;
}
@Override
public boolean isCropSupported()
{
return true;
}
@Override
public LuminanceSource crop(int left, int top, int width, int height)
{
return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);
}
@Override
public boolean isRotateSupported()
{
return true;
}
@Override
public LuminanceSource rotateCounterClockwise()
{
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = rotatedImage.createGraphics();
g.drawImage(image, transform, null);
g.dispose();
int width = getWidth();
return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
}
}
//goto src\main\java\com\fly\qrcode\qr\QRCodeUtil.java
package com.fly.qrcode.qr;
import java.awt.BasicStroke;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.net.URL;
import java.util.Hashtable;
import javax.imageio.ImageIO;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.Result;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class QRCodeUtil
{
private static final String CHARSET = "utf-8";
/**
* 二维码尺寸
*/
private static final int QRCODE_SIZE = 300;
/**
* LOGO宽度
*/
private static final int WIDTH = 60;
/**
* LOGO高度
*/
private static final int HEIGHT = 60;
/**
* 给定内容、图标生成二维码图片
*
* @param content 內容
* @param imgURL 图标
* @param needCompress 是否压缩尺寸
* @return
* @throws Exception
* @see [类、类#方法、类#成员]
*/
public static BufferedImage createImage(String content, URL imgURL, boolean needCompress)
throws Exception
{
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints);
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
if (imgURL == null)
{
return image;
}
// 插入图片
insertImage(image, imgURL, needCompress);
return image;
}
private static void insertImage(BufferedImage source, URL imgURL, boolean needCompress)
throws Exception
{
if (imgURL == null)
{
System.err.println("文件不存在!");
return;
}
Image src = ImageIO.read(imgURL);
int width = src.getWidth(null);
int height = src.getHeight(null);
if (needCompress)
{
// 压缩LOGO
width = Math.min(width, WIDTH);
height = Math.min(height, HEIGHT);
Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
src = image;
}
// 插入LOGO
Graphics2D graph = source.createGraphics();
int x = (QRCODE_SIZE - width) / 2;
int y = (QRCODE_SIZE - height) / 2;
graph.drawImage(src, x, y, width, height, null);
Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
graph.setStroke(new BasicStroke(3f));
graph.draw(shape);
graph.dispose();
}
/**
* 解析二维码图
*
* @param file
* @return
* @throws Exception
* @see [类、类#方法、类#成员]
*/
public static String decode(File file)
throws Exception
{
BufferedImage image = ImageIO.read(file);
if (image == null)
{
return null;
}
BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Hashtable<DecodeHintType, String> hints = new Hashtable<>();
hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
Result result = new MultiFormatReader().decode(bitmap, hints);
return result.getText();
}
/**
* 解析二维码图
*
* @param path
* @return
* @throws Exception
* @see [类、类#方法、类#成员]
*/
public static String decode(String path)
throws Exception
{
return decode(new File(path));
}
}
//goto src\main\java\com\fly\QrCodeApplication.java
package com.fly;
import java.io.IOException;
import org.apache.commons.lang3.SystemUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.fly.core.utils.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringBootApplication
public class QrCodeApplication implements CommandLineRunner
{
public static void main(String[] args)
{
SpringApplication.run(QrCodeApplication.class, args);
}
@Override
public void run(String... args)
throws IOException
{
if (SystemUtils.IS_OS_WINDOWS)
{
log.info("★★★★★★★★ now open Browser ★★★★★★★★ ");
String url = SpringContextUtils.getServerBaseURL();
Runtime.getRuntime().exec("cmd /c start /min " + url + "/doc.html");
}
}
}
//goto src\main\resources\application.yml
server:
port: 8080
servlet:
context-path: /
session:
timeout: 1800
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
//goto src\main\resources\log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="off">
<!-- 日志文件目录和压缩文件 -->
<Properties>
<Property name="fileName">../logs/hello</Property>
<Property name="fileGz">../logs/hello/7z</Property>
</Properties>
<Appenders>
<!--这个输出控制台的配置 -->
<Console name="console" target="SYSTEM_OUT">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="NEUTRAL" />
<!--输出日志的格式 -->
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} %L %M - %msg%xEx%n" />
</Console>
<!--这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
<RollingRandomAccessFile name="rollingInfoFile" fileName="${fileName}/springboot-info.log" immediateFlush="false"
filePattern="${fileGz}/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.springboot-info.gz">
<PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} [%t] %-5level %logger{36} %L %M - %msg%xEx%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="6" modulate="true" />
<SizeBasedTriggeringPolicy size="50 MB" />
</Policies>
<Filters>
<!-- 只记录info级别信息 -->
<ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL" />
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- 指定每天的最大压缩包个数,默认7个,超过了会覆盖之前的 -->
<DefaultRolloverStrategy max="50" />
</RollingRandomAccessFile>
<RollingRandomAccessFile name="rollingErrorFile" fileName="${fileName}/springboot-error.log" immediateFlush="false"
filePattern="${fileGz}/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.springboot-error.gz">
<PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} [%t] %-5level %logger{36} %L %M - %msg%xEx%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="6" modulate="true" />
<SizeBasedTriggeringPolicy size="50 MB" />
</Policies>
<Filters>
<!-- 只记录error级别信息 -->
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- 指定每天的最大压缩包个数,默认7个,超过了会覆盖之前的 -->
<DefaultRolloverStrategy max="50" />
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<!-- 全局配置,默认所有的Logger都继承此配置 -->
<AsyncRoot level="INFO" additivity="false">
<AppenderRef ref="console" />
<AppenderRef ref="rollingInfoFile" />
<AppenderRef ref="rollingErrorFile" />
</AsyncRoot>
</Loggers>
</Configuration>
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-