全网最通俗易懂的【短链接二维码】实战
昨天的文章推送中有一篇题为全网最通俗易懂的【短链接】入门, 让我觉得颇为有趣好玩,这不正好理论知识学完了,实操代码撸起来。如果有不了解的同学可以看看入门那篇的介绍,我这里直接从实战说起,代码中有超过的中文注释,让你更容易阅读理解。话不多说,上代码!
效果展示
项目搭建与相关依赖
新建一个普通的maven java 工程,如图所示
自己给项目取组名(group)和模块名(artifact)
至此项目搭建完成,此时我们为项目引入一些基本的jar包
<properties>
<spring.version>5.1.8.RELEASE</spring.version>
<spring.boot.version>2.1.6.RELEASE</spring.boot.version>
</properties>
<dependencies>
<!-- spring boot依赖,表示这是一个springboot web程序. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
<!-- apache 的工具包. -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
</dependencies>
如何实现短链接生成功能
集成H2内存数据库
- 添加必要的jar包
<!-- 引入H2的相关包. --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> <!--辅助jar包,用于查看内存数据库--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <version>${spring.boot.version}</version> <optional>true</optional> </dependency> - 在application.properties中添加必要的配置
#h2配置 spring.jpa.show-sql = true spring.jpa.hibernate.ddl-auto = update ##数据库连接设置 spring.datasource.url = jdbc:h2:mem:dbtest spring.datasource.username = root spring.datasource.password = root spring.datasource.driverClassName =org.h2.Driver ##数据初始化设置 #进行该配置后,每次启动程序,程序都会运行resources/db/schema.sql文件,对数据库的结构进行操作。 spring.datasource.schema=classpath:db/schema.sql #进行该配置后,每次启动程序,程序都会运行resources/db/data.sql文件,对数据库的数据操作。 #spring.datasource.data=classpath:db/data.sql ##h2 web console设置 spring.datasource.platform=h2 # 进行该配置后,h2 web consloe 就可以在远程访问了。否则只能在本机访问。 spring.h2.console.settings.web-allow-others=true #进行该配置,你就可以通过YOUR_URL/h2访问h2 web consloe。YOUR_URL是你程序的访问URl。 spring.h2.console.path=/h2 #进行该配置,程序开启时就会启动h2 web consloe。当然这是默认的,如果你不想在启动程序时启动h2 web consloe,那么就设置为false。 spring.h2.console.enabled=true - 添加数据库结构脚本
在resource
目录下新建文件夹db
,创建文件schema.sql
,内容如下
create table if not exists short_link ( id int not null primary key, url varchar(1000), create_time DATE ); - 测试H2数据库
启动springboot应用程序,在浏览器中输入http://localhost:2088/h2
,可以打开h2数据库管理器登录界面,能够进入如下页面说明H2集成成功!
输入配置的数据库信息,点击登录,即可打开操作界面:
使用spring data jpa
创建实体与数据库表的映射对象
@Entity
@Data
@NoArgsConstructor
@ToString
public class ShortLink {
@Id
@GeneratedValue
private long id;
private String url;
private Date createTime;
public ShortLink(String url, Date date){
this.url = url;
this.createTime =date;
}
}
创建DAO数据库交互层,CrudRepository
实现了对 DB 基本的增删改查方法
/**
* CrudRepository 实现了对 ShortLink 基本的增删改查方法
* @author jiangpeng
* @date 2019/11/2715:29
*/
public interface ShortLinkRepository extends CrudRepository<ShortLink, Long> {
}
短链接的ID转换生成器
/**
* 短链接生成
* 10进制、62进制互转
* @author jiangpeng
*/
@Slf4j
public class ConversionUtils {
/**
* 初始化 62 进制数据,索引位置代表字符的数值,比如 A代表10,z代表61等
*/
private static String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static int scale = 62;
/**
* 将数字转为62进制
*
* @param num Long 型数字
* @param length 转换后的字符串长度,不足则左侧补0
* @return 62进制字符串
*/
public static String encode(long num, int length) {
StringBuilder sb = new StringBuilder();
int remainder;
// id混淆算法
long snum = num & 0xff000000;
snum += (num & 0x0000ff00) << 8;
snum += (num & 0x00ff0000) >> 8;
snum += (num & 0x0000000f) << 4;
snum += (num & 0x000000f0) >> 4;
while (snum > scale - 1) {
/*
对 scale 进行求余,然后将余数追加至 sb 中,由于是从末位开始追加的,因此最后需要反转(reverse)字符串
*/
remainder = Long.valueOf(snum % scale).intValue();
sb.append(chars.charAt(remainder));
snum = snum / scale;
}
sb.append(chars.charAt(Long.valueOf(snum).intValue()));
String value = sb.reverse().toString();
log.info("encode id: {}", snum);
return StringUtils.leftPad(value, length, '0');
}
/**
* 62进制字符串转为数字
*
* @param str 编码后的62进制字符串
* @return 解码后的 10 进制字符串
*/
public static long decode(String str) {
/*
将 0 开头的字符串进行替换
*/
str = str.replace("^0*", "");
long num = 0;
int index;
for (int i = 0; i < str.length(); i++) {
/*
查找字符的索引位置
*/
index = chars.indexOf(str.charAt(i));
/*
索引位置代表字符的数值
*/
num += (long) (index * (Math.pow(scale, str.length() - i - 1)));
}
// id混淆算法
long snum = num & 0xff000000;
snum += (num & 0x00ff0000) >> 8;
snum += (num & 0x0000ff00) << 8;
snum += (num & 0x000000f0) >> 4;
snum += (num & 0x0000000f) << 4;
return snum;
}
/**
* @param args
*/
public static void main(String[] args) {
System.out.println("62进制:" + encode(1, 5));
System.out.println("10进制:" + decode("0000G"));
}
}
可以执行main方法查看运行结果,比对是否进制转后还是原来的值
集成 freeMarker 静态页面
引入freeMarker的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${spring.boot.version}</version>
</dependency>
freeMarker默认读取模板文件路径为resource/templates目录下,所以在这个目录下创建页面文件 short_link.ftl
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8"/>
<title></title>
</head>
<body>
<h2>短链接转换</h2>
<form action="shortLink" method="post">
要转换的url:<textarea rows="1" style="width: 432px; height: 43px;" name="url">${url?default('')}</textarea>
<br/><br/>
<input type="submit" value="提交"/>
</form>
<#if shortUrl?? && shortUrl != "">
<a href="${shortUrl}" target="_blank">${shortUrl}</a>
</#if>
</body>
</html>
复制代码
创建请求Controller
/**
* 生成短链接请求类
*
* @author jiangpeng
* @date 2019/11/2715:19
*/
@Controller
@RequestMapping("shortLink")
public class ShortLinkController {
@Autowired
private ShortLinkRepository shortLinkRepository;
@GetMapping
public String shortLink() {
return "short_link";
}
/**
* 生成短链接
*
* @param url 要转换的url
* @return short_link.ftl
*/
@PostMapping
public String createShortLink(String url, HttpServletRequest request) throws UnknownHostException {
Instant instant = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant();
ShortLink shortLink = shortLinkRepository.save(new ShortLink(url, Date.from(instant)));
String shortStr = ConversionUtils.encode(shortLink.getId(), 4);
request.setAttribute("url", url);
request.setAttribute("shortUrl", getServerUrl(request) + "/shortLink/" + shortStr);
return "short_link";
}
/**
* 解析短链接并跳转页面
*
* @param shortUrl 短链接参数
*/
@RequestMapping("/{shortUrl}")
public void redirectToSourceUrl(@PathVariable("shortUrl") String shortUrl, HttpServletResponse response) throws IOException {
long id = ConversionUtils.decode(shortUrl);
Optional<ShortLink> shortLinkOpt = shortLinkRepository.findById(id);
String url = shortLinkOpt.orElseGet(null).getUrl();
response.sendRedirect(url);
}
/**
* 获取当前应用服务器域名和端口
* @return String
*/
private String getServerUrl(HttpServletRequest request) throws UnknownHostException {
StringBuilder sb = new StringBuilder();
//获取服务器域名
String serverName = request.getServerName();
//获取服务器端口
int serverPort = request.getServerPort();
//获取服务器IP地址;
String hostAddress = InetAddress.getByName(request.getServerName()).getHostAddress();
return sb.append("http://").append(serverName).append(":").append(serverPort).toString();
}
}
复制代码
最后在浏览器输入http://localhost:2088/shortLink
即可跳转到对应页面,如下图是演示效果
以下是数据库表中保存的数据,ID是其中的短链链接参数生成与转换的关键
如何实现二维码链接功能
使用zxing生成二维码
引入zxing 二维码工具包, 它实现了关于业界二维码的规范
<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>
二维码生成工具类
/**
* 二维码生成工具类
* @author jiangpeng
* @date 2019/11/28 0028
*/
@Slf4j
public class QRCodeUtils {
/**
* 生成二维码
*
* @Param Content 二维码内容
* @Param outputStream
*/
public static void QREncode(String content, File logoFile, OutputStream outputStream) throws WriterException,
IOException {
// 图像宽度
int width = 200;
// 图像高度
int height = 200;
// 图像类型
String format = "png";
Map<EncodeHintType, Object> hints = new HashMap<>();
//内容编码格式
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
// 指定纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
//设置二维码边的空度,非负数
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
/*
问题:生成二维码正常,生成带logo的二维码logo变成黑白; 原因:MatrixToImageConfig默认黑白,需要设置BLACK、WHITE
解决:https://ququjioulai.iteye.com/blog/2254382
*/
if (logoFile != null) {
MatrixToImageConfig matrixToImageConfig = new MatrixToImageConfig(0xFF000001, 0xFFFFFFFF);
BufferedImage bufferedImage = LogoMatrix(MatrixToImageWriter.toBufferedImage(bitMatrix,
matrixToImageConfig),
logoFile);
//输出带logo图片
ImageIO.write(bufferedImage, format, outputStream);
} else {
MatrixToImageWriter.writeToStream(bitMatrix, format, outputStream);
}
log.info("二维码生成成功!");
}
/**
* 识别二维码
*/
public static void QRReader(File file) throws IOException, NotFoundException {
MultiFormatReader formatReader = new MultiFormatReader();
//读取指定的二维码文件
BufferedImage bufferedImage = ImageIO.read(file);
BinaryBitmap binaryBitmap =
new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage)));
//定义二维码参数
Map<DecodeHintType, String> hints = new HashMap<>(8);
hints.put(DecodeHintType.CHARACTER_SET, "utf-8");
Result result = formatReader.decode(binaryBitmap, hints);
//输出相关的二维码信息
log.info("解析结果:" + result.toString());
log.info("二维码格式类型:" + result.getBarcodeFormat());
log.info("二维码文本内容:" + result.getText());
bufferedImage.flush();
}
/**
* 二维码添加logo
*
* @param matrixImage 源二维码图片
* @param logoFile logo图片
* @return 返回带有logo的二维码图片
*/
public static BufferedImage LogoMatrix(BufferedImage matrixImage, File logoFile) throws IOException {
/*
* 读取二维码图片,并构建绘图对象
*/
Graphics2D g2 = matrixImage.createGraphics();
int matrixWidth = matrixImage.getWidth();
int matrixHeight = matrixImage.getHeight();
/*
* 读取Logo图片
*/
BufferedImage logo = ImageIO.read(logoFile);
int logoWidth = matrixWidth / 4;
int logoHeight = matrixHeight / 4;
int x = matrixWidth / 10 * 4;
int y = matrixHeight / 10 * 4;
//开始绘制图片
g2.drawImage(logo, x, y, logoWidth, logoHeight, null);
BasicStroke stroke = new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
// 设置笔画对象
g2.setStroke(stroke);
//指定弧度的圆角矩形
RoundRectangle2D.Float round = new RoundRectangle2D.Float(x, y, logoWidth, logoHeight, 20, 20);
g2.setColor(Color.white);
// 绘制圆弧矩形
g2.draw(round);
//设置logo 有一道灰色边框
BasicStroke stroke2 = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
// 设置笔画对象
g2.setStroke(stroke2);
RoundRectangle2D.Float round2 = new RoundRectangle2D.Float(x + 2, y + 2, logoWidth - 4, logoHeight - 4, 20, 20);
g2.setColor(new Color(128, 128, 128));
// 绘制圆弧矩形
g2.draw(round2);
g2.dispose();
matrixImage.flush();
return matrixImage;
}
}
使用 freeMarker 静态页面
freeMarker默认读取模板文件路径为resource/templates目录下,所以在这个目录下创建页面文件 qr_code.ftl
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8"/>
<title></title>
</head>
<body>
<h2>二维码内容生成</h2>
<form action="qrCode" method="post" enctype="multipart/form-data">
<span>内容:<input type="text" name="content"></span>
<br/><br/>
<span>logo:<input type="file" name="logoFile"></span>
<br/><br/>
<input type="submit" value="提交">
</form>
</body>
</html>
创建请求Controller
/**
* 生成二维码请求类
* @author jiangpeng
* @date 2019/11/28 0028
*/
@Controller
@RequestMapping("qrCode")
public class QRCodeController {
/**
* 跳转页面
* @return
*/
@GetMapping
public String qrCode(){
return "qr_code";
}
/**
* 生成二维码
* @param content 内容
* @param response HttpServletResponse
*/
@PostMapping
public void createQrCode(String content, @RequestParam("logoFile") MultipartFile logoFile, HttpServletResponse response) throws Exception {
File file = !logoFile.isEmpty() ? FileConvertUtils.multipartFileToFile(logoFile): null;
QRCodeUtils.QREncode(content, file, response.getOutputStream());
}
}
复制代码
最后在浏览器输入http://localhost:2088/shortLink即可跳转到对应页面,如下图是演示效果