2024年10月


一并附上 Mybatis 和 Mybatis Plus 的使用区别
MyBatis Mapper.XML 标签使用说明

Pom 依赖

Mybatis

<!-- 统一管理 jar 包版本 -->
<properties>
    <druid-boot.version>1.1.10</druid-boot.version>
    <mybatis-boot.version>2.1.0</mybatis-boot.version>
    <mysql-connector.version>8.0.16</mysql-connector.version>
    <mssql-jdbc.version>8.2.2.jre8</mssql-jdbc.version>
    <oracle-jdbc.version>19.3.0.0</oracle-jdbc.version>
    <pagehelper-starter.version>1.2.10</pagehelper-starter.version>
</properties>

<!--子模块继承之后,锁定版本+子模块不用写 groupid 和 version -->
<dependencyManagement>
    <dependencies>
        <!-- mybatis + druid + mysql + mssql-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${pagehelper-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector.version}</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>${mssql-jdbc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>${oracle-jdbc.version}</version>
        </dependency>
        <!-- mybatis + druid + mysql + mssql-->
    </dependencies>
</dependencyManagement>

Mybatis Plus
使用框架自带的分布控件,如果使用
pagehelper
会报
JSqlParser
的版本冲突,根据情况排除
pagehelper
版本(不推荐)。

<!-- 统一管理 jar 包版本 -->
<properties>
    <druid-boot.version>1.2.23</druid-boot.version>
    <mybatis-plus.version>3.5.7</mybatis-plus.version>
    <mysql-connector.version>8.0.33</mysql-connector.version>
    <mssql-jdbc.version>8.2.2.jre8</mssql-jdbc.version>
    <oracle-jdbc.version>19.3.0.0</oracle-jdbc.version>
</properties>

<!--子模块继承之后,锁定版本+子模块不用写 groupid 和 version -->
<dependencyManagement>
    <dependencies>
        <!-- mybatis plus + druid + mysql + mssql-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <!--分页-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-annotation</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql-connector.version}</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>${mssql-jdbc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>${oracle-jdbc.version}</version>
        </dependency>
        <!-- mybatis plus + druid + mysql + mssql-->
    </dependencies>
</dependencyManagement>

yml 配置

Mybatis

mybatis:
  # 指定sql映射文件位置
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-handlers-package: com.vipsoft.base.handler # MySQL 8.0  用以mysql中json格式的字段,进行转换的自定义转换器,转换为实体类的JSONObject属性

Mybatis-Plus

mybatis-plus:
  mapper-locations: classpath*:mapper/*Mapper.xml
  global-config:
    banner: true
    db-config:
      id-type: auto
      where-strategy: not_empty
      insert-strategy: not_empty
      update-strategy: not_null
  type-handlers-package: com.vipsoft.base.handler # MySQL 8.0  用以mysql中json格式的字段,进行转换的自定义转换器,转换为实体类的JSONObject属性
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    jdbc-type-for-null: 'null'
    call-setters-on-nulls: true
    shrink-whitespaces-in-sql: true

druid
SpringBoot 配置多数据源

spring:
  profiles:
    active: dev
  resources:
    static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/web/,file:${cuwor.file.path}

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    #数据源基本配置
    url: jdbc:mysql://192.168.1.100:3306/production_education?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL
    username: root
    password: root
    #连接池的设置
    druid:
      initial-size: 5 #初始化时建立物理连接的个数
      min-idle: 5  #最小连接池数量
      max-active: 200  #最大连接池数量 maxIdle已经不再使用
      max-wait: 60000 #获取连接时最大等待时间,单位毫秒
      test-while-idle: true #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      time-between-eviction-runs-millis: 60000 #既作为检测的间隔时间又作为testWhileIdel执行的依据
      #销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
      min-evictable-idle-time-millis: 30000
      validation-query: select 'x' #用来检测连接是否有效的sql 必须是一个查询语句( mysql中为 select 'x'  oracle中为 select 1 from dual)
      test-on-borrow: false #申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
      test-on-return: false  #归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
      #exception-sorter: true #当数据库抛出不可恢复的异常时,抛弃该连接
      #pool-prepared-statements: true  #是否缓存preparedStatement,mysql5.5+建议开启
      max-pool-prepared-statement-per-connection-size: 20  #当值大于0时poolPreparedStatements会自动修改为true
      filters: stat,wall #配置扩展插件
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 #通过connectProperties属性来打开mergeSql功能;慢SQL记录
      use-global-data-source-stat: true #合并多个DruidDataSource的监控数据
      #设置访问druid监控页的账号和密码,默认没有--放DrugConfig配置中
      #stat-view-servlet.login-username: admin
      #stat-view-servlet.login-password: admin

Config 配置

Druid 配置没有变化

package com.vipsoft.base.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DruidConfig {

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druid(){
        return  new DruidDataSource();
    }

    //配置Druid的监控
    //1、配置一个管理后台的Servlet
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        Map<String,String> initParams = new HashMap<>();

        initParams.put("loginUsername","admin");
        initParams.put("loginPassword","vipsoft");
        initParams.put("resetEnable","false");
        initParams.put("allow","");//默认就是允许所有访问
        initParams.put("deny","192.168.15.21"); //IP黑名单(同时存在时,deny优先于allow)

        bean.setInitParameters(initParams);
        return bean;
    }


    //2、配置一个web监控的filter
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());

        Map<String,String> initParams = new HashMap<>();
        initParams.put("exclusions","*.js,*.css,/druid/*");

        bean.setInitParameters(initParams);

        bean.setUrlPatterns(Arrays.asList("/*"));

        return  bean;
    }
}

Mybatis Plus 分页,
需要添加 拦截器配置,否则分页不生效

package com.vipsoft.base.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    /**
     * 分页插件 -- 否则分页不生效
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

image

Mapper扫描

@MapperScan({"com.vipsoft.admin.mapper"}) 和 Mybatis 无区别

package com.vipsoft.admin;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; 
import org.springframework.context.annotation.ComponentScan;


@ComponentScan(basePackages = {"com.vipsoft"})
@SpringBootApplication
@MapperScan({"com.vipsoft.admin.mapper"})
public class VipSoftAdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(VipSoftAdminApplication.class, args);
    }
}

Entity

SysMenu

//@TableName("sys_menu") 默认会将 SysMenu 解析成 sys_menu 如果解析后不是正确的表名,需要通过 TableName进行指定,
public class SysMenu extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 菜单ID */
	@TableId(value = "menu_id", type = IdType.ASSIGN_ID)
    private Long menuId;

    /** 菜单名称 */
	@TableField(value = "menu_name")
    private String menuName;

    /** 父菜单名称 */
    @TableField(exist = false)  //非数据库字段,进行排除
    private String parentName;
	....省略
}

Mapper.xml

SysMenuMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vipsoft.admin.mapper.SysMenuMapper">

    <resultMap type="com.vipsoft.admin.entity.SysMenu" id="SysMenuResult">
        <id property="menuId" column="menu_id"/>
        <result property="menuName" column="menu_name"/>
        <result property="parentName" column="parent_name"/>
        <result property="parentId" column="parent_id"/>
        <result property="orderNum" column="order_num"/>
        <result property="path" column="path"/> 
        <result property="createTime" column="create_time"/>
        <result property="updateTime" column="update_time"/> 
    </resultMap>

    <select id="listMenu" parameterType="com.vipsoft.admin.entity.SysMenu" resultMap="SysMenuResult">
        select menu_id, menu_name, parent_id, order_num, create_time from sys_menu
        <where>
            <if test="menuName != null and menuName != ''">
                AND menu_name like concat('%', #{menuName}, '%')
            </if>
            <if test="visible != null and visible != ''">
                AND visible = #{visible}
            </if>
            <if test="status != null and status != ''">
                AND status = #{status}
            </if>
        </where>
        order by parent_id, order_num
    </select>

    <select id="listMenuPage" resultMap="SysMenuResult">
        select menu_id, menu_name, parent_id, order_num, create_time from sys_menu
        <where>
            <if test="query.menuName != null and query.menuName != ''">
                AND menu_name like concat('%', #{query.menuName}, '%')
            </if>
            <if test="query.visible != null and query.visible != ''">
                AND visible = #{query.visible}
            </if>
            <if test="query.status != null and query.status != ''">
                AND status = #{query.status}
            </if>
        </where>
        order by parent_id, order_num
    </select>
</mapper>

image

Mapper - SysMenuMapper

需要继承
BaseMapper

package com.vipsoft.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vipsoft.admin.entity.SysMenu;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 菜单表 数据层
 */
public interface SysMenuMapper extends BaseMapper<SysMenu> {

    List<SysMenu> listMenu(SysMenu menu);

    IPage<SysMenu> listMenuPage(Page page, @Param("query") SysMenu menu);
}

Service

ISysMenuService

package com.vipsoft.admin.service;


import com.baomidou.mybatisplus.core.metadata.IPage; 
import com.vipsoft.admin.entity.SysMenu; 

import java.util.List; 

/**
 * 菜单 业务层
 *
 */
public interface ISysMenuService
{
    /**
     * 列表查询(自定义SQL,分页)
     */
    List<SysMenu> listMenu(SysMenu menu);

    /**
     * 列表查询(框架分页)
     */
    IPage  selectPage(SysMenu menu);


    /**
     * 列表查询(自定义SQL,分页)
     */
    IPage listMenuPage(SysMenu menu);
}

SysMenuService

package com.vipsoft.admin.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vipsoft.admin.entity.SysMenu;
import com.vipsoft.admin.mapper.SysMenuMapper;
import com.vipsoft.admin.service.ISysMenuService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.github.pagehelper.PageParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 菜单 业务层处理
 *
 * @author ruoyi
 */
@Service
public class SysMenuServiceImpl implements ISysMenuService { 

    @Autowired
    private SysMenuMapper menuMapper;

    /**
     * 列表查询(自定义SQL,分页)
     */
    @Override
    public List<SysMenu> listMenu(SysMenu menu) {
        return menuMapper.listMenu(new SysMenu());
    }

    /**
     * 列表查询(框架分页)
     */
    @Override
    public IPage selectPage(SysMenu menu) {
        Page page = new Page();
        page.setCurrent(2);
        page.setSize(10);
        List<OrderItem> orderItems = new ArrayList<>();
        orderItems.add(OrderItem.desc("menu_id"));
        page.setOrders(orderItems);
        Page pageList = menuMapper.selectPage(page, null);
        return pageList;
    }

    /**
     * 列表查询(自定义SQL,分页)
     */
    @Override
    public IPage listMenuPage(SysMenu menu) {
        Page page = new Page();
        page.setCurrent(2);
        page.setSize(10);
        List<OrderItem> orderItems = new ArrayList<>();
        orderItems.add(OrderItem.desc("menu_id")); //先按 menu_id 排序,再按 mapper.xml 中的排(可以在查询输出的SQL中查看)
        page.setOrders(orderItems);
        IPage<SysMenu> pageList = menuMapper.listMenuPage(page, menu);
        return pageList;
    }
}

Controller

package com.vipsoft.admin.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.vipsoft.admin.entity.SysMenu;
import com.vipsoft.admin.service.ISysMenuService;
import com.vipsoft.base.core.ApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 菜单信息
 */
@RestController
@RequestMapping("/menu")
public class SysMenuController {
    @Autowired
    private ISysMenuService menuService;

    /**
     * 获取菜单列表
     */
    @GetMapping("/selectPage")
    public ApiResult selectPage(SysMenu menu) {
        IPage menus = menuService.selectPage(menu);
        return new ApiResult(menus);
    }

    /**
     * 获取菜单列表
     */
    @GetMapping("/list")
    public ApiResult listMenu(SysMenu menu) {
        List<SysMenu> menus = menuService.listMenu(menu);
        return new ApiResult(menus);
    }


    /**
     * 获取菜单列表
     */
    @GetMapping("/listMenuPage")
    public ApiResult listMenuPage(SysMenu menu) {
        IPage menus = menuService.listMenuPage(menu);
        return new ApiResult(menus);
    }
}

简要介绍

Shadcn UI
与其他 UI 和组件库如
Material UI

Ant Design

Element UI
的设计理念截然不同。这些库一般通过
npm
包提供对组件的访问,而
Shadcn UI
允许用户将单个 UI 组件的源代码直接下载到项目中,提供了更大的灵活性和定制空间。
按照
Shadcn UI
的说法,
Shadcn UI
实际上并不是一个组件库,而是可以复制并粘贴到应用中的可重用组件的集合。

显著特性

  • 简洁且易于使用
    :Shadcn UI为用户提供了直观且易于理解的文档,可以轻松地开始使用。它不需要复杂的配置步骤,只需简单的复制粘贴或使用CLI安装即可快速集成到项目中。与其他组件库相比,Shadcn UI简化了开发流程,
    降低了学习曲线
    ,可以专注于构建应用的核心功能。
  • 卓越的可访问性
    :Shadcn UI 在设计之初就充分考虑到了可访问性,确保其组件符合Web内容可访问性指南(WCAG)标准。这意味着使用Shadcn UI构建的应用程序不仅外观美观,而且能够适应各种用户需求,无论是使用屏幕阅读器、键盘导航还是其他辅助技术的用户都能顺利使用。
  • 精细控制与高度可定制
    :与其他UI库不同,Shadcn UI允许
    直接访问每个组件的源代码
    。这意味着可以根据项目的具体需求轻松调整代码,而无需受限于预定义的模板或样式。这种高度的定制性提供了更大的灵活性,可以轻松地调整组件的外观、行为和功能,以满足项目的独特要求。此外,这种可定制性还简化了应用 的扩展和维护工作,使得长期开发变得更加高效。

使用方式

  1. 设置项目
npx shadcn-ui@latest init
// or
npx shadcn-ui@latest init
// or
pnpm dlx shadcn-ui@latest init
  1. 添加组件
npx shadcn-ui@latest add button
// or
npx shadcn-ui@latest add button
// or
pnpm dlx shadcn-ui@latest add button

上面的命令会将
Button
组件添加到您的项目中(components目录下)。然后您可以像这样导入它:

import { Button } from "@/components/ui/button"

export default function Home() {
  return (
    <div>
      <Button>Click me</Button>
    </div>
  )
}

适用场景

  1. 企业级应用

Shadcn UI
提供了高度可扩展和可定制的组件,非常适合用于构建复杂的企业级应用程序。它允许开发者快速搭建复杂的用户界面,同时保持代码的可维护性和一致性。

  1. 个性化品牌设计

如果你的项目需要符合品牌视觉设计,
Shadcn UI
是一个理想的选择。通过其灵活的样式系统,你可以轻松定制组件的外观,确保界面与品牌风格统一。这在构建品牌官网、个人博客或产品展示网站时尤为适用。

  1. 移动优先的应用

Shadcn UI
提供了默认的响应式布局,非常适合移动优先的应用开发。如果你的项目需要兼顾移动设备和桌面设备,
Shadcn UI
能够轻松适配不同的屏幕尺寸,帮助你打造优秀的用户体验。

  1. 快速原型开发

得益于其简单直观的组件结构和开箱即用的 UI 元素,
Shadcn UI
非常适合快速原型开发。如果你需要在短时间内构建一个功能齐全的前端界面,
Shadcn UI
可以帮助你快速完成任务。

  1. 性能要求高的应用

Shadcn UI
的轻量化设计,使其非常适合对性能有严格要求的应用场景。电商平台、实时数据更新系统以及需要快速响应的单页应用(SPA)都能从中受益。

为什么选择 Shadcn UI?

Shadcn UI
作为一个现代化的 UI 组件库,具有极高的灵活性、可定制性和轻量化特性。相比其他庞大且复杂的 UI 库,
Shadcn UI
提供了更加简洁的解决方案,帮助开发者减少不必要的代码和样式,同时确保界面组件的高效性和易用性。

此外,
Shadcn UI
的社区和文档也在持续成长和完善。对于开发者来说,它不仅是一个简单的工具库,更是一个不断更新和进化的生态系统,确保能够跟上技术的变化。

如果你的项目需要一个高效、灵活、轻量的 UI 组件库来快速构建现代化界面,同时具备高度的可定制性,
Shadcn UI
无疑是一个值得选择的工具。

总结

Shadcn UI
是现代前端开发者不可多得的高效组件库,它结合了极简设计与高度可定制性,帮助开发者快速构建高性能、响应式的界面。无论你是在构建企业级应用、品牌官网,还是需要快速开发原型,
Shadcn UI
都能够为你提供优秀的解决方案。


该框架已经收录到我的全栈前端一站式开发平台 “前端视界” 中(浏览器搜 前端视界 第一个),感兴趣的欢迎浏览使用!

Java 当中使用 “google.zxing ”开源项目 和 “github 的 qrcode-plugin” 开源项目 生成二维码

@


1. Java当中使用 “google.zxing ” 开源项目生成二维码

这里我们使用 servlet 和 tomcat 技术进行操作演示。

1.1 准备工作

首先在
pom.xml
文件当中导入相应所需的依赖。

在这里插入图片描述

<dependencies>
        <!--        添加 jakarta.servlet 框架 -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!--zxing依赖-->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!--commons-lang依赖-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>

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.rainbowsea</groupId>
    <artifactId>zxingQRcode</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!--        添加 jakarta.servlet 框架 -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!--zxing依赖-->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!--commons-lang依赖-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

注意:我们是使用的是 tomcat 和 servlet 我们需要注意
web
的根路径是否正确,如果错误的话,是无法找到对应资源的。

对应的前端显示的页面代码的编写内容,这里我们使用
jsp
作为前端页面的展示。
注意:前端页面所放的路径是在
web
目录下的

在这里插入图片描述

<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<!DOCTYPE html>

<html lang="en" >
<head>
    <meta charset="UTF-8"/>
    <title>生成普通黑白二维码</title>
</head>
<body>
url:<input type="text" id="url">
<hr/>
<%--请输入文本内容:--%>
<%--<textarea id="url" cols="60" rows="20"></textarea>--%>
<button onclick="generateQRcode()">生成二维码</button>
<br>
<img id="image"/>
<script>
    function generateQRcode() {
        let url = document.getElementById("url").value;
        let i = document.getElementById("image");
        i.src = "/zxingQRcode/create?url=" + url;
    }
</script>
</body>
</html>

1.2 生成黑白二维码

1.2.1 zxing 常用 API

1.2.1.1 EncodeHintType (编码提示类型)

在这里插入图片描述

EncodeHintType 是用来设置二维码编码时的一些额外参数的枚举类型,常用枚举值如下:

  1. ERROR_CORRECTION

    1. 误差校正级别
      。对于黑白二维码,可选值
      L(7%)

      M(15%)

      Q(25%)

      H(30%)
      ,表示二维码允许破损的最大容错率。在二维码出现破损时,根据设置的容错率级别,可以尝试设置修复二维码中的一些数据。
    2. 二维码在生成过程中,可能会出现一些破损或者是缺失的情况,例如:“打印时墨水耗尽,图像压缩,摄像头拍摄角度不对”等。这些问题可能导致二维码无法完全识别,或者识别出来的数据不准确,而误差校正码就是为了解决这些问题而产生的。
    3. 例如:选择
      L
      级别的容错率,相当于允许在二维码的整体颜色区域中,最多有约
      7%
      的坏像素点;而选择
      H
      级别的容错率时,最多可有
      30%
      的坏像素点。
  2. CHARACTER_SET
    :表示编码字符集,可以设置使用的字符编码,例如:utf-8,ag2312等等。
  3. MARGIN
    :二维码的空白区域大小,可以设置二维码周围的留白大小,以便于在不同的嵌入场景中使用二维码。

1.2.1.2 MultiFormatWriter(多格式写入程序)

在这里插入图片描述

MultiFormatWriter
是一个便捷的二维码生成类,可以根据传入的
BarcodeFormat
参数,生成对应类型的二维码。

MultiFormatWriter
封装了一系列的二维码生成方法,可以生成多种格式的二维码,包括:“QR Code、Aztec Code、PDF417、Data Matrix”等。

1.2.1.3 BarcodeFormat(码格式)

BarcodeFormat 是枚举类,通过它来制定二维码格式:

  1. QR Code
    :QR Code 是最常见的二维码格式之一,广泛应用于商品包装,票务,扫码支付等领域。QR Code 矩阵有黑白两种颜色,其中黑色部分表示信息的编码,白
    在这里插入图片描述
    色部分,则用于衬托和辨识。
  2. Aztec Code
    :Aztec Code 是一种高密度,可靠性很高的二维码格式。相比于其他二维码格式,它具有更低的容错率,更小的尺寸和更高的解码效率。因此,它适合用于存储一些核心信息,例如:个人信息,证件信息,账户密码等。
  3. PDF417
    :是一种可以存储大量信息的二维码格式,它具有数据密度高,可靠性强等优点,可以应用于许多场景,例如:航空机票,运输和配送标签,法律文件等。
  4. Data Matrix
    :是一种小巧的二维码格式,它的编码方式类似于 QR Code,但是其可靠性,识别率,扫描速度和牢固度都比 QR Code 更优秀。由于尺寸较小,可靠新较高,因此 Data Matrix 适合嵌入简单的产品标签,医疗图像,检测数据等领域。

1.2.1.4 BitMatrix(位矩阵)

在这里插入图片描述

BitMatrix
是 ZXing 库中表示二维码矩阵的数据结构,它是由 0 和 1 构成的二维数组,用于存储二维码的编码信息。在二维码生成过程中,我们通过对
BitMatrix
对象的构建和操作,最终生成一个可被扫描解码的二维码图像。

BitMatrix
实际上是一个
紧凑型的布尔型二维数组
,往往只需要占用一个字节即可表示
S
位二进制。在使用
BitMatrix
时,我们可以通过其不同的方法,例如:
get()

set()
等,来获取,设置矩阵中每个位置的值。

在 ZXing中,
BitMatrix
常用于将编码后的信息转化为矩阵形式,并进行图像的生成和输出。在使用 ZXing 生成二维码时,我们首先需要使用
MultiFormatWriter.encode()

方法来生成一个
BitMatrix
;然后,在对
BitMatrix
进行各种处理和操作后,就可以在 UI中显示和输出二维码。

总的来说,
BitMatrix
是ZXing 库中非常重要的数据结构之一,它负责存储和处理生成二维码图像所需的二进制信息,是实现二维码生成功能的关键。

BitMatrix 常用 API:

  • getHeight()
    : 获取矩阵高度
  • getWidth()
    :获取矩阵宽度
  • get(x,y)
    :根据 "x,y" 的坐标获取矩阵中该坐标的值。结果是
    true(黑色)
    或者是
    false(白色)

1.2.2 将 url 生成黑白二维码

前端代码:

在这里插入图片描述

<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<!DOCTYPE html>

<html lang="en" >
<head>
    <meta charset="UTF-8"/>
    <title>生成普通黑白二维码</title>
</head>
<body>
url:<input type="text" id="url">
<hr/>
<%--请输入文本内容:--%>
<%--<textarea id="url" cols="60" rows="20"></textarea>--%>
<button onclick="generateQRcode()">生成二维码</button>
<br>
<img id="image"/>
<script>
    function generateQRcode() {
        let url = document.getElementById("url").value;
        let i = document.getElementById("image");
        i.src = "/zxingQRcode/create?url=" + url;
    }
</script>
</body>
</html>

后端代码:

定义为一个 Servlet 进行处理前端的请求,返回一个二维码。

在这里插入图片描述

在这里插入图片描述

package com.rainbowsea.servlets;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


@WebServlet("/create")
public class GenerateQRcode extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 使用谷歌提供的 zxing开源库,生成普通的黑白二维码(核心代码)


        try {
            // 需要创建一个Map集合,用这个 Map集合存储二维码相关的属性(参数)
            Map map = new HashMap();
            // 设置二维码的误差校正级别
            map.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
            // 设置二维码的字符集
            map.put(EncodeHintType.CHARACTER_SET, "utf-8");
            // 设置二维码四周的留白
            map.put(EncodeHintType.MARGIN, 1);

            // 创建 zxing的核心对象, MultiFormatWriter (多格式写入器)
            // 通过 MultiFormatWriter 对象来生成二维码
            MultiFormatWriter writer = new MultiFormatWriter();

            // 获取文本内容
            String url = request.getParameter("url");
            //writer.encode(内容,什么格式的二维码,二维码的宽度,二维码的高度,二维码的参数)
            // 位矩阵对象
            BitMatrix bitMatrix = writer.encode(url, BarcodeFormat.QR_CODE, 300, 300, map);

            // 获取该位矩阵的宽度
            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++) {
                    // 0xFF000000 : 0xFFFFFFFF 表示 黑白的十六进制
                    image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
                }
            }

            // 将图片响应到浏览器客户端上
            ServletOutputStream out = response.getOutputStream();
            ImageIO.write(image,"png",out);
            out.flush();
            out.close();

        } catch (WriterException e) {
            e.printStackTrace();
        }

    }
}

运行显示效果:

在这里插入图片描述

我们也可以生成一个以
文本内容
生成的一个二维码。如下:修改一下前端的提交的格式内容即可。

在这里插入图片描述

<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<!DOCTYPE html>

<html lang="en" >
<head>
    <meta charset="UTF-8"/>
    <title>生成普通黑白二维码</title>
</head>
<body>
<%--url:<input type="text" id="url">--%>
<hr/>
请输入文本内容:
<textarea id="url" cols="60" rows="20"></textarea>
<button onclick="generateQRcode()">生成二维码</button>
<br>
<img id="image"/>
<script>
    function generateQRcode() {
        let url = document.getElementById("url").value;
        let i = document.getElementById("image");
        i.src = "/zxingQRcode/create?url=" + url;
    }
</script>
</body>
</html>

后端代码 Servlet 不动,这样。

运行效果如下:

在这里插入图片描述

1.3 生成一个带 logo 的黑白二维码

前端的页面展示的代码处理:

在这里插入图片描述

<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<!DOCTYPE html>

<html lang="en" >
<head>
    <meta charset="UTF-8"/>
    <title>生成Log二维码</title>
</head>
<body>
<%-- 大数据用: multipart/form-data--%>
<form action="/zxingQRcode/generateWithLogon" method="post" enctype="multipart/form-data">
    请输入url: <input type="text" name="url"><br>
    请选择图片: <input type="file" name="logo"><br>
    <input type="submit" value="生成带Logo的二维码">
</form>

</body>
</html>

后端 Servlet 代码的内容的编写,如下:

在这里插入图片描述

package com.rainbowsea.servlets;


import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Part;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

@WebServlet("/generateWithLogon")
// 设置传输文件的最大值和最小值
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 2, maxFileSize = 1024 * 1024 * 10, maxRequestSize = 1024 * 1024 * 100)
public class GerateQRcodeWithLogon extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
            IOException {
        // 使用谷歌提供的 zxing开源库,生成普通的黑白二维码(核心代码)


        try {
            // 需要创建一个Map集合,用这个 Map集合存储二维码相关的属性(参数)
            Map map = new HashMap();
            // 设置二维码的误差校正级别
            map.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
            // 设置二维码的字符集
            map.put(EncodeHintType.CHARACTER_SET, "utf-8");
            // 设置二维码四周的留白
            map.put(EncodeHintType.MARGIN, 1);

            // 创建 zxing的核心对象, MultiFormatWriter (多格式写入器)
            // 通过 MultiFormatWriter 对象来生成二维码
            MultiFormatWriter writer = new MultiFormatWriter();

            // 获取文本内容
            String url = request.getParameter("url");
            //writer.encode(内容,什么格式的二维码,二维码的宽度,二维码的高度,二维码的参数)
            // 位矩阵对象
            BitMatrix bitMatrix = writer.encode(url, BarcodeFormat.QR_CODE, 300, 300, map);

            // 获取该位矩阵的宽度
            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++) {
                    // 0xFF000000 : 0xFFFFFFFF 表示 黑白的十六进制
                    image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
                }
            }

            // 给二维码添加Logo
            // 第一部分: 将Logo缩放
            Part logoPart = request.getPart("logo");
            InputStream inputStream = logoPart.getInputStream();
            BufferedImage logoImage = ImageIO.read(inputStream);

            // 对获取到的logo图片进行缩放
            int logoWidth = logoImage.getWidth(null);
            int logoHeight = logoImage.getHeight(null);
            // 对 logo 图片的大小进行一个缩放,整理
            if (logoWidth > 60) {
                logoWidth = 60;
            }

            if (logoHeight > 60) {
                logoHeight = 60;
            }

            // 这一行代码非常重要,全靠它来进行缩放了
            // 使用平滑缩放算法对原始的Logo图像进行缩放得到一个全新的图像
            Image scaledLogo = logoImage.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH);

            // 第二部分: 将缩放后的Logo画到黑白二维码上
            // 获取一个2D的画笔
            Graphics2D graphics2D = image.createGraphics();
            // 指定 logo 图片从哪里开始,也就是指定开始的坐标 x,y
            int x = (300 - logoWidth) / 2;
            int y = (300 - logoHeight) / 2;

            // 将缩放之后的logo画上去
            // 第一个参数是:logo缩放后的图片,二三参数是:坐标,第四个参数是: null
            graphics2D.drawImage(scaledLogo,x,y,null);
            // 创建一个具有指定位置,宽度,高度和圆角半径的圆角矩形,这个圆角矩形是用来绘制边框的
            Shape shape = new RoundRectangle2D.Float(x, y, logoWidth, logoHeight, 10, 10);
            // 使用一个宽度为 4 像素的基本笔触
            graphics2D.setStroke(new BasicStroke(4f));
            // 给 logo 画圆角矩形
            graphics2D.draw(shape);

            // 释放画笔
            graphics2D.dispose();

            // 将二维码图片响应到浏览器上
            ImageIO.write(image, "png", response.getOutputStream());

        } catch (WriterException e) {
            e.printStackTrace();
        }


    }
}

运行效果
在这里插入图片描述

2. Java当中使用 “github 的 qrcode-plugin” 开源项目生成二维码

github 当中的 "qrcode-plugin" 开源项目,比较友好,只需要简单调用接口。

对应的 xml 文件依赖如下:

<dependency>
    <groupId>com.github.liuyueyi.media</groupId>
    <artifactId>qrcode-plugin</artifactId>
    <version>2.5.2</version>
</dependency>

2.1 准备工作:

首先在
pom.xml
文件当中导入相应所需的依赖。

在这里插入图片描述


    <dependencies>
        <!--        添加 jakarta.servlet 框架 -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!--commons-lang依赖-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

<!--        github 二维码开源项目-->
        <dependency>
            <groupId>com.github.liuyueyi.media</groupId>
            <artifactId>qrcode-plugin</artifactId>
            <version>2.5.2</version>
        </dependency>

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.rainbowsea</groupId>
    <artifactId>githubliuyueyi</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!--        添加 jakarta.servlet 框架 -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!--commons-lang依赖-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

<!--        github 二维码开源项目-->
        <dependency>
            <groupId>com.github.liuyueyi.media</groupId>
            <artifactId>qrcode-plugin</artifactId>
            <version>2.5.2</version>
        </dependency>
    </dependencies>

</project>

注意:我们是使用的是 tomcat 和 servlet 我们需要注意
web
的根路径是否正确,如果错误的话,是无法找到对应资源的。

在这里插入图片描述

对应的前端显示的页面代码的编写内容,这里我们使用
jsp
作为前端页面的展示。
注意:前端页面所放的路径是在
web
目录下的

在这里插入图片描述

<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>github 开源项目生成酷炫二维码</title>
</head>
<body>
<form action="/githubliuyueyi/generateWithLogon" method="post" enctype="multipart/form-data">
    请输入url: <input type="text" name="url"><br>
    请选择图片: <input type="file" name="logo"><br>
    <input type="submit" value="生成二维码">
</form>
</body>
</html>

2.2 生成黑白二维码

在这里插入图片描述


import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

@WebServlet("/generateWithLogon")
// 设置传输文件的最大值和最小值
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 2,
        maxFileSize = 1024 * 1024 * 10,
        maxRequestSize = 1024 * 1024 * 100)
public class GenerateWithQrCode extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            String url = request.getParameter("url");

            // 生成黑白二维码
            BufferedImage image = QrCodeGenWrapper.of(url)
                    .asBufferedImage();
            // 将二维码图片响应到浏览器
            ImageIO.write(image, "png", response.getOutputStream());


        } catch (WriterException e) {
            e.printStackTrace();
        }

    }
}

运行显示效果:

在这里插入图片描述

2.3 生成带有 Logo 的二维码

在这里插入图片描述


import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

@WebServlet("/generateWithLogon")
// 设置传输文件的最大值和最小值
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 2,
        maxFileSize = 1024 * 1024 * 10,
        maxRequestSize = 1024 * 1024 * 100)
public class GenerateWithQrCode extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            String url = request.getParameter("url");


            // 生成带有 logo 的黑白二维码
            BufferedImage image = QrCodeGenWrapper.of(url)
                    .setLogo(request.getPart("logo").getInputStream())
                    .setLogoRate(7) // 设置Logo图片与二维码之间的比例,7表示Logo的宽度等于二维码的 1/7
                    .setLogoStyle(QrCodeOptions.LogoStyle.ROUND) // 设置logo图片的样式,将logo的边框形状设置为圆锐角
                    .asBufferedImage();
            
            // 将二维码图片响应到浏览器
            ImageIO.write(image, "png", response.getOutputStream());


        } catch (WriterException e) {
            e.printStackTrace();
        }

    }
}

运行效果:

在这里插入图片描述

2.4 生成彩色的二维码

在这里插入图片描述

package com.rainbowsea.servlets;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

@WebServlet("/generateWithLogon")
// 设置传输文件的最大值和最小值
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 2,
        maxFileSize = 1024 * 1024 * 10,
        maxRequestSize = 1024 * 1024 * 100)
public class GenerateWithQrCode extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            String url = request.getParameter("url");

            BufferedImage image = QrCodeGenWrapper.of(url)
                    .setDrawPreColor(Color.BLUE)
                    .asBufferedImage();

            ImageIO.write(image, "png", response.getOutputStream());


        } catch (WriterException e) {
            e.printStackTrace();
        }

    }
}

在这里插入图片描述

2.5 背景图的二维码

在这里插入图片描述

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

@WebServlet("/generateWithLogon")
// 设置传输文件的最大值和最小值
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 2,
        maxFileSize = 1024 * 1024 * 10,
        maxRequestSize = 1024 * 1024 * 100)
public class GenerateWithQrCode extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            String url = request.getParameter("url");
            // 生成带有背景图的二维码
            BufferedImage image = QrCodeGenWrapper.of(url)
                    .setBgImg(request.getPart("logo").getInputStream())
                    .setBgOpacity(0.5F)  // 设置背景图片的透明度
                    .asBufferedImage();

            
            // 将二维码图片响应到浏览器
            ImageIO.write(image, "png", response.getOutputStream());


        } catch (WriterException e) {
            e.printStackTrace();
        }

    }
}

运行效果:

在这里插入图片描述

2.6 特殊形状二维码

在这里插入图片描述

运行效果:

在这里插入图片描述

2.7 图片填充二维码

在这里插入图片描述

package com.rainbowsea.servlets;

import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeDeWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper;
import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions;
import com.google.zxing.WriterException;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

@WebServlet("/generateWithLogon")
// 设置传输文件的最大值和最小值
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 2,
        maxFileSize = 1024 * 1024 * 10,
        maxRequestSize = 1024 * 1024 * 100)
public class GenerateWithQrCode extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            String url = request.getParameter("url");

            BufferedImage image = QrCodeGenWrapper.of(url)
                    .setErrorCorrection(ErrorCorrectionLevel.H) // 设置二维码的错误纠正规则
                    .setDrawStyle(QrCodeOptions.DrawStyle.IMAGE) // 绘制二维码时采用图片填充
                    .addImg(1, 1, request.getPart("logo").getInputStream()) // 添加图片
                    .asBufferedImage();

            ImageIO.write(image, "png", response.getOutputStream());


        } catch (WriterException e) {
            e.printStackTrace();
        }

    }
}

运行效果:

在这里插入图片描述

2.8 生成 gif 动图二维码

String url = request.getParameter("url");

BufferedImage image = QrCodeGenWrapper.of(url)
        .setW(500)
        .setH(500)
        .setBgImg(request.getPart("logo").getInputStream())
        .setBgOpacity(0.6f)
        .setPicType("gif")
        .asBufferedImage();

ImageIO.write(image, "gif", response.getOutputStream());

v

大家好,我是汤师爷~

今天系统性地聊聊SaaS应用架构设计。

应用架构概述

我们已经完成了SaaS系统的定位分析,明确了系统的目标和核心能力。这为接下来的应用架构设计奠定了基础。

应用架构就像整个SaaS系统的骨架,决定了系统的整体结构和各个组件之间的关系。接下来,我们会深入探讨应用架构的三个核心要素:应用服务、应用结构和应用交互。这些要素共同构成了一个体系化的SaaS系统架构。如图所示。

通常,应用架构设计包括以下几个步骤:

  • 识别应用服务:根据业务架构,把业务需求转化为IT系统,找出关键的应用服务。
  • 划分应用结构:设计应用结构,以及与业务流程、数据之间的关系,明确各部分的职责。
  • 设计应用交互:规划各个应用结构之间如何交互和集成,确保系统各部分协调运作。

应用服务设计

在设计应用服务之前,我们先搞清楚什么是应用服务,以及重要性。

应用服务的概念

应用服务是对一个或一组密切相关的业务对象及其操作的封装。

应用服务要明确定义自己的责任范围,将相关业务功能和对象组合在一起,避免暴露内部细节。

它需要整合因为同一原因变化的功能和数据,同时分离那些因为不同原因变化的部分。这样的设计方法,确保了服务的内聚性和灵活性。

应用服务的概念源自SOA(面向服务的架构)和微服务架构的兴起。通过把系统功能拆分成多个独立的服务,可以提高系统的可维护性、可扩展性和灵活性。

应用服务如何划分?

应用服务在应用架构中非常重要,它把系统的核心功能“打包”起来,提供给外部的业务流程使用,可以看作是SaaS系统对外的“门面”。用户或者其他系统通过调用应用服务来实现特定的业务功能。那么,怎么设计应用服务呢?

1、对齐业务能力,划分粒度适中的应用服务,职责单一。

在划分应用服务粒度时,可以参考领域驱动设计(DDD)中的"限界上下文"概念。业务对象类似于限界上下文中的聚合根,是应用服务的核心。

通常情况下,我们会基于业务能力来划分应用服务,每个业务能力都对应一到多个独立的应用服务,每个应用服务用于支撑特定的业务能力。如图所示。

将应用服务与业务能力对齐,确保系统功能紧密贴合业务需求,避免技术实现与业务逻辑脱节。

如果一个应用服务支撑了过多的业务能力,需要评估其内部是否关联了过多的业务对象。在这种情况下,可以考虑将多个业务对象进行分组,从而将该应用服务拆分为多个更小、更专注的服务。

2、围绕业务对象,提供具体的业务功能,避免包含不相关的功能。

从外部来看,应用服务通常有明确的业务含义,主要围绕一个或一组密切相关的业务对象进行操作。

围绕业务对象设计服务可确保服务内部功能高度相关,提升内聚性。提供具体的业务功能让服务的边界更清晰,有利于业务团队和技术团队的协作与沟通。

例如,线上商城系统的”交易服务”专注于订单确认、下单和支付等功能,不应处理用户认证、商品推荐等其他业务。

服务化架构的价值

面向服务的架构最大的价值就在于它的敏捷性和灵活性。

敏捷性体现在服务可以快速调整,独立演进。灵活性体现在每个服务都有清晰的业务边界,功能内聚性强,能够单独管理生命周期。具体来说:

  1. 轻量级应用:采用基于服务设计的轻应用,各个服务独立开发、部署和运营,可以独立交付,影响范围小,提升交付效率。
  2. 服务复用、灵活编排:通过服务的复用和灵活编排,可以快速响应业务的变化,支持复杂的业务流程。
  3. 局部扩展性高:系统被拆分为独立的服务后,容易进行横向扩展,只需要扩展必要的部分,成本更低,效果更好。

示例:订单履约应用服务划分

如图所示,订单履约能力是零售企业业务能力地图中的 L2 级别业务能力。

订单履约能力可以细分为多个末级业务能力:面向消费者的履约服务、订单派单、订单管理、拣货管理、发货管理和逆向履约。

基于这些末级业务能力,我们就可以设计出对应的应用服务。

应用结构设计

在完成应用服务的设计后,我们需要深入地了解应用的内部结构。

应用结构设计是把应用服务的概念转化为具体实现的关键步骤。它描述了应用服务内部的层次结构和组织关系,决定了系统的模块化程度,以及后续开发和维护的难度。

应用结构的抽象层次

在设计应用结构时,我们通常会把系统分成不同的层次,比如系统级、应用级、模块级和代码级。

这种分层的方式有助于我们在不同层面处理复杂问题,确保系统结构清晰、易于维护。如图所示:

  • 系统级:
    关注各个系统的整体布局和管理方式,比如系统之间的关系,以及它们如何协同工作。
  • 应用级:
    聚焦每个应用的整体架构,包括应用与其他应用的交互方式,以及它们在整个系统中的角色。
  • 模块级:
    对应用内部进行更细致的划分,涉及代码的模块化设计、数据和状态的管理等。通过合理的模块划分,可以提高代码的可维护性和可重用性,减少重复工作。
  • 代码级:
    关注代码本身的结构和实现方式,这一层的设计直接影响到代码的质量和实现细节。

抽象层次的存在,是为了帮我们更有效地管理系统的复杂性。

通过把系统分成不同的抽象层次,我们可以更好地组织和理解系统结构,简化开发过程,提高代码的可维护性和可扩展性。

这种分层方法让开发团队能在不同层次上专注于特定的问题,更好地应对大型软件系统的挑战。具体来说有以下作用:

1、分解复杂度

如果把所有的业务细节、技术细节都混在一起,整个系统就会变得难以理解、维护和扩展。通过设置不同的抽象层次,我们可以把系统的复杂性分解到各个层次,每个层次只需关注特定的功能和职责。

这种分层处理方式让开发人员在专注于系统某一部分时,不用过多关注其他部分的细节,大大简化了系统的设计和开发过程。

2、团队协作边界清晰

在大型项目中,通常会有多个团队同时开发。如果系统没有明确的边界,各团队之间很容易产生冲突和重复劳动。

通过清晰的抽象层次划分,不同团队可以专注于系统的不同层次或模块,互不干扰。

3、扩展性强

随着业务需求的变化,系统往往需要不断地扩展和升级。如果系统的架构设计没有合理的抽象层次,扩展和升级就会变得非常困难,甚至可能需要对系统进行全面重构。

而在有抽象层次的系统中,变更通常只需聚焦在特定的层次上进行,而不会影响整个系统。比如,一次业务改动只影响模块级别,我们就可以在不改变系统整体架构的情况下,替换或新增某个模块,满足新的业务需求。

应用结构如何划分?

在前面,我们提到了应用服务的设计方法,那么怎么把这些应用服务一步一步地转化成代码结构呢?

其实,应用服务是通过一系列的应用结构来实现的。如图所示。

基于应用服务的划分,我们可以进一步细化应用结构,更好地组织和管理系统功能。这个过程涉及到多个层次的设计方法:

  1. 系统和子系统的划分要和业务域、业务子域的粒度保持一致。 这样,我们就能更好地把业务需求映射到技术实现上。
  2. 一个或多个相关的应用服务,可以组合成一个可独立部署的应用。
  3. 在应用内部,可以进一步分层。 比如,参考领域驱动设计(DDD)的分层方法,可以分为用户接口层、应用层、领域层和基础设施层。
  4. 应用的各个层级内部,还可以细分为多个模块,每个模块又包含多个代码单元。

那么,具体来说,我们该怎么划分应用呢?可以参考以下几点原则:

1、业务划分原则

应用划分的关键是看应用服务的边界。

应用服务的核心目标是帮助企业实现业务能力,所以它们需要和业务能力保持一致。而应用是实际的物理部署单元,应用服务最终要部署在特定的应用上。

因此,一个或多个相关的应用服务,可以组合成一个可独立部署的应用。

应用服务可以单独部署,也可以多个服务合并部署。那么,如何判断何时选择独立部署,何时选择合并部署呢?这需要参考技术层面的成本和稳定性风险等因素。

2、技术划分原则

在业务初期,尽量从单体应用开始,避免过早地把应用拆得太细,这样可以减少分布式数据库事务和数据不一致的问题,并可以降低技术部署成本。然而,即使在单体应用内部,也需要将应用服务划分为界限分明的模块。

避免应用之间出现循环依赖或双向依赖。在应用单元内部,可以进一步分层,始终保持不同层级之间的单向依赖关系,高层级可以依赖低层级,同层级之间不应互相依赖。

只有当真正遇到技术上的痛点,比如规模、性能、安全等问题时,才考虑拆分应用。如果不拆分会严重影响业务的稳定性,那就应该拆分。但不要为了拆分而拆分,因为每次拆分都会增加系统的复杂度。

3、组织规模原则

划分后的单个应用的项目团队规模通常建议保持在10~12人左右。

因为团队成员越多,沟通的渠道就会成倍增加,可能导致信息传递变慢或者失真。一个10到12人的团队,可以确保大家的沟通更直接、更高效,减少信息障碍。

小团队更容易管理,项目经理或者团队领导能更好地了解每个成员的工作状态和需求,进行更有效的协调和支持。

同时,小团队有助于建立更紧密的合作关系,成员之间更容易培养出默契,提升整体工作效率和项目质量。

5.5.5 示例:订单履约系统的应用结构划分

如图所示,是订单履约系统的应用结构划分。

  • 用户接口层:直接与用户交互的层级,负责向用户显示信息,响应用户命令。
  • 应用层:定义软件的应用功能,它负责接收用户请求,协调领域层能力来执行任务,并将结果返回给用户,核心模块包括:
  • 领域层:业务逻辑的核心,专注于表示业务概念、业务状态流转和业务规则,沉淀可复用的服务能力。

应用交互设计

应用交互就是指不同应用之间怎么“沟通”和“交流”。

在一个复杂的系统中,各个应用可不是孤立存在的,它们需要互相配合,才能完成更复杂的业务流程。

应用交互的设计,就是为了确保这些系统和组件能够顺畅地“对话”,一起实现系统的整体目标。

应用交互的方式有很多种,包括同步调用、异步消息通信等等。每种方式都有特定的应用场景和优缺点。

通过合理的交互设计,系统中的各个部分能够高效协作,降低耦合度,增强系统的灵活性。

同时,好的交互设计还能显著提升系统的性能和容错能力,即使在高并发、大流量的情况下,也能保持稳定运行。

应用服务的上下游

应用服务是系统对外提供的核心业务功能。

虽然应用服务可以独立发展和演化,但它们必须相互交互,才能实现整个系统目标。

那么,如何设计应用服务之间的交互呢?首先,我们需要了解服务的上下游概念。

1、服务上下游的概念

服务的上下游关系可以通过DDD(领域驱动设计)的建模方法来定义,主要涉及“限界上下文”(bounded context)和“上下文映射”(context mapping)这两个概念。

上下游表示上下文之间的映射关系,下游需要了解上游的领域知识来实现业务,而上游不需要了解下游。

换句话说,上游服务不需要关心下游服务的存在,但下游服务的实现却依赖于上游服务提供的能力。

这个概念听起来有些抽象,确实让许多人犯迷糊。让我们通过线上商城的几个应用服务来具体说明:

  • 用户服务:管理用户的账户信息,包括注册、登录、认证、个人资料等,处理用户的权限和角色管理。
  • 商品服务:管理商品的基本信息,包括名称、描述、价格、图片、分类等,提供商品的查询、筛选和浏览功能。
  • 库存服务:管理商品的库存数量,处理库存的预占、扣减和回补操作。
  • 交易服务:处理订单的创建、修改、取消和查询,管理订单的状态和生命周期。
  • 支付服务:处理支付事务,支持多种支付方式,管理支付状态。
  • 履约服务:处理订单的履约,包括拣货、包装、发货等,管理物流信息和配送状态。

如图所示,我们可以看出各个服务的上下游关系。

商品服务和用户服务是上游服务,它们提供基础数据,其他服务依赖这些数据。

交易服务位于中间位置。对于用户服务和商品服务来说,交易服务是下游,因为它依赖这两个服务的基础数据。

对于库存服务来说,交易服务也是下游,因为交易下单过程中,需要库存服务来预占、扣减库存。

对于履约服务而言,交易服务是上游,因为它提供订单数据,驱动后续的订单履约流程。

2、为什么要区分上下游?

区分上下游关系的核心目标是为了解耦。

"解耦"这个词相信大家都不陌生,但它的含义往往过于抽象和模糊。在这里,我们探讨一下解耦到底指什么。

耦合是指两个或多个结构之间的相互作用和影响。在软件开发中,这可以理解为不同模块、系统或团队之间的相互依赖和影响。

随着业务需求越来越复杂,单个系统或团队很难独立实现所有功能。因此,解耦的目的并不是完全消除耦合,而是减少不必要的依赖关系。

前面提到,上游服务不需要关心下游服务的存在,但下游服务的实现却依赖上游服务提供的能力。

因此,当下游服务的团队在迭代新功能时,无需评估是否影响上游服务,因为基于明确的上下游关系,可以快速判断不会影响上游服务。只需评估是否影响自己的下游服务。

比如,交易服务的功能发生变更时,只需通知履约服务的团队,评估是否会影响到他们,上游服务团队则无需知晓。

这种方式能大大减少影响面的评估工作,提高团队协作效率。

相反,如果上下游关系混乱,存在各种循环依赖,那么任何一个服务的改动都难以准确评估影响面。此时,就需要召集所有服务的团队,逐一评估是否有影响。

在实际场景中,如果每次项目会议都需要拉一大群人才能评估出影响面,这样的协作效率就太低了。

3、上下游关系的核心使用场景

在软件研发过程中,上下游关系在很多关键场景中都发挥着重要作用:

  • 明确服务之间的依赖关系:上下游关系让开发者清晰地了解服务间的依赖。这有助于减少不合理的依赖,确保服务的独立性和模块化设计。同时,它也避免了服务间的循环依赖,降低了一个服务出现故障引发连锁反应的风险。
  • 评估影响面:当上游服务变更时,可以预见其对下游服务的影响,从而制定相应的应对策略。
  • 指导团队协作:上下游关系有助于明确各团队的职责和工作范围。上游团队需要考虑下游团队的需求,提供稳定的接口和服务;下游团队则需要适应上游的变化。

应用服务的交互方式

应用服务之间的交互方式有很多,最主要的就是同步调用和异步消息。

1、同步调用

同步调用是一种通信方式,调用方(客户端)向被调用方(服务端)发送请求,然后等待服务端处理完再返回结果。

在这期间,调用方会被“堵住”,直到收到服务端的响应。这种方式要求双方都在线,而且调用方在等待响应时,没法做别的事。

在微服务架构中,常用的同步调用协议包括 HTTP、REST API、Dubbo、Thrift、gRPC 和 SOAP 等。

同步调用适用于下游服务需要立刻从上游服务获取数据或功能的场景。这种方式简单直接,但需要处理服务之间的可用性问题。

举个例子,用户下单时,订单服务需要同步调用商品服务,获取商品的最新价格和库存信息,确保订单有效。

通常来说,上游服务不应同步调用下游服务。如果上游服务同步调用下游服务,会导致上游需要了解下游的领域知识,违背DDD上下游的设计原则,加深系统耦合,并增加团队协作复杂性。

此外,这种做法还可能引发级联故障,降低系统可靠性。如果上下游直接互相调用,那下游服务发生故障时,也将直接影响上游服务的可用性,可能导致整个系统都瘫痪。

2、异步消息

异步消息是另一种通信方式,消息的发送者(生产者)和接收者(消费者)通过消息队列或消息中间件进行通信。

发送者发完消息就可继续其他操作,不用等接收者处理完。消息被发送到消息队列后,接收者从队列中异步获取并处理。这样一来,发送者和接收者的处理时间就不耦合了,双方可以各自独立运作,提高了系统的灵活性和可扩展性。

在微服务架构中,异步消息通常通过消息中间件实现,比如 RabbitMQ、Kafka、RocketMQ 等。

异步消息适用于上游服务向下游服务发布事件或通知的场景,能有效解耦服务,提高系统的弹性和可靠性。下游服务也可以通过异步消息向上游服务反馈信息,实现双向通信。

比如,用户提交订单后,订单服务调用支付服务发起支付。用户完成支付后,支付服务发布一个“支付成功”的消息,订单服务接收到消息后,更新订单状态并发送通知。

3、其他交互方式

1)共享数据库方式

多个服务访问同一个数据库,直接读取或写入数据。

在微服务架构中,通常不建议采用共享数据库的方式,因为这违反了服务自治的原则,增加了服务之间的耦合度。

2)文件传输

服务之间通过共享文件系统或 FTP 等方式交换数据文件。这种方式一般适用于批处理的场景,实时性较差。

3)服务总线(ESB)

使用统一的通信总线来连接不同的服务和系统。服务之间不直接通信,而是通过总线来“中转”,适用于需要集成多种异构系统和服务的大型企业级系统。

但是,这种方式引入了额外的架构层,增加了系统的复杂性。所有服务都耦合到总线上,存在单点故障的风险。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

大家好,我是小富~

有个兄弟私下跟我说,他在面试狗东时,有一道面试题没回答上来:Redis 的
Bitmap

布隆过滤器
啥区别与关系?

其实就是考小老弟对这两种工具的底层数据结构是否了解,不算太难的题。不过,bitmap和布隆过滤器在大数据量和高并发业务的使用频率不低,知识点应该掌握下,既然问了那咱们简单的梳理下它们的底层原理、应用场景以及它们之间的关联。

Bitmap

Redis中的Bitmap(位图)是一种较为特殊数据类型,它以最小单位
bit
来存储数据,我们知道一个字节由 8个 bit 组成,和传统数据结构用字节存储相比,这使得它在处理大量二值状态(true、false 或 0、1等只有两种状态)数据时具有极高的空间效率。不过,它不是一种全新的数据类型,其底层实现仍是基于
String
类型。

便于理解,你可以将 Bitmap 的底层结构看成是由一系列 bit 位组成的数组,在此数组中,每个位都对应一个偏移量(类似数组的下标)。通过将特定偏移量上的位值设置为 0 或 1,来表示不同的状态。

比如我们要设计一个答题游戏系统。其规则为:若用户答对全部 7 道题,则可获得大奖。

每个答题用户都有自己的 key,即
answer:user:X
。使用 bitmap 记录用户的答题情况,将题号设置为对应偏移量,当用户答对 ✅ 题目时 ,偏移量位值设为 1;当用户答错 ❌ 题目时,位值设为 0。

假如用户
user:1
答对了 2、5、7 号题,可将对应偏移量为 2、5、7 的位值设置为 1,其余位值默认设为 0。若要查看该用户对某个题目的回答情况,只需按照偏移量遍历此数据结构,一旦碰到位值为 1 的情况,即表示该题回答正确。

答题活动结束后,接下来需要统计获奖者,即那些全部答对 7 道题的用户。

要快速统计用户是否全部答对题目,可以使用
BITCOUNT
命令来统计位值中被设置为 1 的数量。通过执行
BITCOUNT answer:userX == 7
这样的操作进行判断,若结果等于 7,则表明该用户全部答对了题目。

聪明的你或许会产生疑惑,如果想用 bitmap 判断邮箱地址是否在黑名单内,偏移量该如何设置呢?遗憾的是,bitmap 并不支持直接以字符串作为偏移量。不过,我们可以对邮箱进行哈希运算得到哈希值,进而算出偏移量。

由于用到哈希运算,就不可避免地会出现数据碰撞问题,即不同的字符串可能得出相同的哈希值。这样一来,状态判断就可能不准确。别急,后边介绍布隆过滤器(
Bloom Filter
)看它如何来优化这个问题。

操作命令

Bitmap 的操作命令不多且使用简单,主要用到的就是
SETBIT

GETBIT

BITCOUNT

BITOP
几个命令。

SETBIT
:用于设置指定偏移量上的位值,其时间复杂度为 O (1)。例如,当用户答对了第 7 题时,可以将题号对应的偏移量为 7 的位值设置为 1,以此表示该题已被答对。

# 用户key answer:user:1
# 偏移量:题号 7 
# 题答对,置为 1
SETBIT answer:user:1 7 1 

GETBIT
:获取指定偏移量上的位值,同样具有高效的时间复杂度。可以快速查询用户对特定题号的回答状态。

# 查询用户第 7 题的回答情况,1-答对 0-答错
GETBIT answer:user:1 7

BITCOUNT
:用于统计位值中被设置为 1 的数量。比如上边我可以很容易统计答对全部题目的用户,但也仅能知道个数,无法查看具体的哪个题目。

# 统计用户答对题为 1 的个数
BITCOUNT answer:user:1 

BITOP
:对一个或多个 bitmap 进行位运算,并将结果保存到新的键中,支持 AND、OR、NOT、XOR 四种操作。这个命令的用法是将多个bitmap中相同偏移量的位值进行运算。若我想知道用户 1 和用户 2 都答对的题目,经过 AND 运算后,假如只有题号 1 是两个用户都答对的题目,那么生成新的结果集中就只有题号 1 对应的位值为 1。

# 用户1 和 用户2 都答对的题目,可以看出只有题号1的都答对了
SETBIT answer:user:1 1 1
SETBIT answer:user:1 2 0
SETBIT answer:user:1 3 1

SETBIT answer:user:2 1 1
SETBIT answer:user:2 2 1
SETBIT answer:user:2 3 0

BITOP AND resultbitmap answer:user:1 answer:user:2

扬长避短

优点

  • 极高空间效率:bitmap 是真的节省数据存储空间。粗略的算一下,一亿位的 Bitmap 大概才占 12MB 的内存,相比其他数据结构,能极大地节省存储空间;

  • 快速查询:位操作通常比其他数据结构查询速度更快。无论是设置位值还是获取位值,时间复杂度都为 O (1),能够快速响应查询请求;

  • 易于操作:支持单个位操作、位统计、位逻辑运算等,运算效率高,不需要进行比较和移位;

缺点

  • 由于数据结构特点,导致它仅适用于表示两种状态,即 0 和 1。对于需要表示更多状态的情况,Bitmap 就不适用了;

  • 只有当数据比较密集时才有优势,如果我们只设置(20,30,888888888)三个偏移量的位值,则需要创建一个 99999999 长度的 BitMap ,但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入另一个
    Roaring BitMap
    来解决

应用场景

看到 Bitmap 还是比较简单的一种数据结构,占用空间小查询效率高,非常适用于记录状态的场景,它的应用场景很常见,比如:

  • 用户签到状态(连续签到天数)

  • 用户的在线状态(统计活跃用户)

  • 问卷答题等等吧!

布隆过滤器

上边咱们提到 bitmap 记录字符元素的状态时,需要先借助哈希运算得出偏移量。但引入哈希运算后可能会出现哈希碰撞的情况,导致状态误判。

布隆过滤器对这个问题做了进一步的优化,做到了可控误判率,当我们将一个邮箱地址添加到集合中,多个不同的哈希函数会将这个邮箱地址映射到 bitmap 中的不同偏移量位置上,且将这些位值置为 1。

要判断邮箱地址是否在集合中,通过相同的哈希函数映射到 bitmap 上的多个位置,
如果这些位上的值都为 1,则邮箱
可能存在
集合中;如果有任何一个位置的值为 0,则元素
一定不在
集合中

。这是布隆过滤器的特点。

虽然但是布隆过滤器还是会发生误判的情况,额~,但好在我们可以通过
调整布隆过滤器的大小和哈希函数的数量来控制误判率

操作命令

布隆过滤器的命令也不多,主要用到的如下几个:

BF.RESERVE
:创建一个新的布隆过滤器,并指定容量 capacity 和误判率 error_rate。

BF.RESERVE <key> <error_rate> <capacity>
BF.RESERVE myfilter 0.000001 999999

BF.INFO
:获取布隆过滤器的信息,包括容量、误判率等。

BF.INFO <key>

BF.ADD

BF.MADD
:分别是向布隆过滤器中添加元素和批量添加

# 向布隆过滤器中添加元素
BF.ADD myfilter hello
BF.MADD <key> <item> [item ...]

BF.EXISTS

BF.MEXISTS
:分别是检查布隆过滤器中某个元素和批量检查元素是否存在

# 元素是否存在于布隆过滤器中
BF.EXISTS myfilter hello
# 元素是否存在于布隆过滤器中
BF.MEXISTS <key> <item> [item ...]

扬长避短

优点

  • 布隆过滤器的空间占用也是极小,它本身不存储完整的数据,和 bitmap 一样底层也是通过 bit 位来表示数据是否存在。

  • 性能比较稳定,无论集合中元素的数量有多少,插入和查询操作的时间复杂度都非常低,通常为 O (k),其中 k 是哈希函数的个数。也就是说在处理大规模数据时,布隆过滤器的性能不会随着数据量的增加而急剧下降。

缺点

  • 存在一定的误识别率:布隆过滤器存在误判的情况,即当一个元素实际上不在集合中时,有可能被判断为在集合中。这是因为多个元素可能通过哈希函数映射到相同的位置,导致误判。但是,当布隆过滤器判断一个元素不在集合中时,则是 100% 正确的。

  • 删除元素比较困难:一般情况下,不能直接从布隆过滤器中删除元素。这是因为一个位置可能被多个元素映射到,如果直接将该位置的值置为 0,可能会影响其他元素的判断。

应用场景

布隆过滤器存在一定的误判,所以使用它的场景就一定要允许不准确的情况发生:

  • 解决 Redis 缓存穿透问题:秒杀商品详情通常会被缓存到 Redis 中。如果有大量恶意请求查询不存在的商品,通过布隆过滤器可以快速判断这些商品不存在,从而避免了对数据库的查询,减轻了数据库的压力。

  • 邮箱黑名单过滤:在邮件系统中,可以使用布隆过滤器来过滤垃圾邮件和恶意邮件。将已知的垃圾邮件发送者的地址或特征存储在布隆过滤器中,新邮件来时判断发送者是否在黑名单中。

  • 对爬虫网址进行过滤:在爬虫程序中,为了避免重复抓取相同的网址,可以使用布隆过滤器来记录已经抓取过的网址。新网址出现时,先判断是否已抓取过。

  • 太多太多了.....

总结

一起梳理了 bitmap 和 布隆过滤器的原理、用法以及它们各自的优缺点和应用场景,大环境不好更要多多提升自身技术能力,而且现在面试三句不离大数据量和高并发,此类问题想要应对自如,不仅要有深度还要有广度,掌握这两个知识点多提供一种答案也是好的。写的不好大家对付看吧!