全网最通俗易懂的【短链接二维码】实战

3,207 阅读5分钟

昨天的文章推送中有一篇题为全网最通俗易懂的【短链接】入门, 让我觉得颇为有趣好玩,这不正好理论知识学完了,实操代码撸起来。如果有不了解的同学可以看看入门那篇的介绍,我这里直接从实战说起,代码中有超过的中文注释,让你更容易阅读理解。话不多说,上代码!

效果展示

项目搭建与相关依赖

新建一个普通的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内存数据库

  1. 添加必要的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>
    
  2. 在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
    
  3. 添加数据库结构脚本

    resource目录下新建文件夹db,创建文件schema.sql,内容如下

    create table if not exists short_link (
            id int not null primary key,
            url varchar(1000),
            create_time DATE );
    
    
  4. 测试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即可跳转到对应页面,如下图是演示效果

扫码关注公众号,回复20191128获取本文所有源码

写作不易,如果文章对你有帮助,可否留下脚印留个赞~

☞☞点击这里购买云服务器☜体验代码效果☜