微服务开发系列——第一篇:项目搭建(保姆级教程)

news2024/11/27 0:37:15

总概

A、技术栈

  • 开发语言:Java 1.8
  • 数据库:MySQL、Redis、MongoDB、Elasticsearch
  • 微服务框架:Spring Cloud Alibaba
  • 微服务网关:Spring Cloud Gateway
  • 服务注册和配置中心:Nacos
  • 分布式事务:Seata
  • 链路追踪框架:Sleuth
  • 服务降级与熔断:Sentinel
  • ORM框架:MyBatis-Plus
  • 分布式任务调度平台:XXL-JOB
  • 消息中间件:RocketMQ
  • 分布式锁:Redisson
  • 权限:OAuth2
  • DevOps:Jenkins、Docker、K8S

B、本节实现目标

  • 搭建ac-mall2-cloud微服务基础骨架。
  • 搭建微服务子项目:mall-pom、mall-common、mall-member、mall-product。
  • MyBatis-Plus配置:雪花ID、创建时间/修改时间 自动填充。
  • 单个微服务子项目Swagger配置及访问。
  • 返回JSON数据日期格式化。
  • Swagger优化:mall-common支持多个微服务Swagger配置、Swagger传参(语言参数、token、测试账号)

一、新建项目目录

新建项目目录ac-mall2-cloud,该目录并列存放所有微服务。

二、创建mall-pom父级依赖工程

2.1 创建项目

2.2 选择maven项目

2.3 项目名称和路径

2.4 配置.gitignore文件

.idea
target
*.iml

2.5 删除src目录,配置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>

   <!-- 创建mall-member微服务时会自动配置 -->
    <modules>
        <module>../mall-member</module>
    </modules>

    <groupId>com.ac</groupId>
    <artifactId>mall-pom</artifactId>
    <version>${mall.version}</version>
    <name>mall-pom</name>
    <packaging>pom</packaging>
    <description>基础pom依赖包</description>

    <!-- lombok要与mapstruct版本匹配,用同一时间的版本,不然会出现各种问题 -->
    <properties>
        <mall.version>1.0-SNAPSHOT</mall.version>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
        <mysql.version>8.0.17</mysql.version>
        <mybatis.plus.version>3.2.0</mybatis.plus.version>
        <druid.version>1.1.10</druid.version>
        <boot.version>2.3.6.RELEASE</boot.version>
        <alibaba.cloud.version>2.2.3.RELEASE</alibaba.cloud.version>
        <lombok.version>1.18.6</lombok.version>
        <org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
        <org.projectlombok.version>1.18.6</org.projectlombok.version>
        <swagger2.version>2.9.2</swagger2.version>
        <hibernate-validator.version>6.0.17.Final</hibernate-validator.version>
        <jwt.version>0.9.1</jwt.version>
        <fastjson.version>1.2.62</fastjson.version>
        <commons.version>3.9</commons.version>
        <mybatis.version>3.5.3</mybatis.version>
        <hutool.version>5.1.4</hutool.version>
    </properties>

    <!-- 管理子类所有的jar包的版本,这样的目的是方便去统一升级和维护 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${boot.version}</version>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <scope>provided</scope>
            </dependency>

            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-jdk8</artifactId>
                <version>${org.mapstruct.version}</version>
            </dependency>

            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${org.mapstruct.version}</version>
            </dependency>

            <!--swagger2 start-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${swagger2.version}</version>
            </dependency>

            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${swagger2.version}</version>
            </dependency>
            <!--swagger2 end-->

            <dependency>
                <groupId>org.hibernate.validator</groupId>
                <artifactId>hibernate-validator</artifactId>
                <version>${hibernate-validator.version}</version>
            </dependency>

            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jwt.version}</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons.version}</version>
            </dependency>

            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <!-- 所有的子工程都会自动加入下面的依赖  -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>${boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${alibaba.cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
        </dependency>

        <!--swagger2 start-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>
        <!--swagger2 end-->

        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
    </dependencies>

    <!-- SpringBoot 工程编译打包的插件,放在父pom中就直接给所有子工程继承 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${boot.version}</version>
            </plugin>
        </plugins>
    </build>

</project>

2.6 modules问题

在目前模式下,所有微服务都会在mall-pom.xml下自动生成<modules>依赖,而且采用的是相对路径。后面我们可以搭建私有maven仓库来处理,替换掉<modules>依赖,即将:

<modules>
   <module>../mall-member</module>
</modules>

替换成:

<distributionManagement>
      <repository>
            <id>maven-public</id>
            <name>maven-public</name>
            <url>http://192.168.100.74:8081/repository/maven-public/</url>
      </repository>
</distributionManagement>

三、创建mall-member微服务工程

3.1 创建module

3.2 maven项目

3.3 项目名称和路径

3.4 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>

    <parent>
        <artifactId>mall-pom</artifactId>
        <groupId>com.ac</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <groupId>com.ac</groupId>
    <artifactId>mall-member</artifactId>
    <version>${mall.version}</version>
    <name>mall-member</name>
    <description>用户服务</description>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.5 entity包

entity包放实体bean

package com.ac.member.entity;

import com.ac.member.enums.MemberSexEnum;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("t_member")
public class Member {

    @ApiModelProperty("ID")
    private Long id;

    @ApiModelProperty("用户姓名")
    private String memberName;

    @ApiModelProperty("手机号")
    private String mobile;

    @ApiModelProperty("性别")
    private MemberSexEnum sex;

    @ApiModelProperty("生日")
    private LocalDate birthday;

    @ApiModelProperty("逻辑删除标志")
    @TableLogic
    private Boolean deleted;

    @ApiModelProperty("数据插入时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @ApiModelProperty("数据修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

3.6 mapper包

mapper包放mybatisplus.BaseMapper子类,与个mapper.xml自定义sql接口对应

package com.ac.member.mapper;

import com.ac.member.dto.MemberDTO;
import com.ac.member.entity.Member;
import com.ac.member.qry.MemberQry;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @description 用户-数据访问层(依赖mybatis-plus),对应mapper.xml里的自定义sql
 */
public interface MemberMapper extends BaseMapper<Member> {

    /**
     * 查询用户
     *
     * @param qry
     * @return
     */
    List<MemberDTO> searchMember(@Param("qry") MemberQry qry);
}

3.7 resources/mapper包

放mapper.xml对应的sql语句,Member.Mapper.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.ac.member.mapper.MemberMapper">

    <sql id="MemberDTO_Column">
        t.id,
        t.member_name,
        t.mobile,
        t.sex,
        t.birthday
    </sql>

    <select id="searchMember" resultType="com.ac.member.dto.MemberDTO">
        select
            <include refid="MemberDTO_Column"></include>
        from
            t_member t
        <where>
            t.deleted = 0
            <if test="qry.memberName!=null and qry.memberName!=''">
                and t.member_name like concat('%', #{qry.memberName}, '%')
            </if>
            <if test="qry.mobile!=null and qry.mobile!=''">
                and t.mobile = #{qry.mobile}
            </if>
        </where>
        order by t.create_time desc
    </select>
</mapper>

3.8 dao包

dao为数据访问层(依赖mybatis-plus),重用mybatis-service提供的CURD方法

package com.ac.member.dao;

import com.ac.member.dto.MemberDTO;
import com.ac.member.entity.Member;
import com.ac.member.qry.MemberQry;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
 * @author 阳光倾洒
 * @description 用户-数据访问层(依赖mybatis-plus),重用mybatis-service提供的CURD方法
 */
public interface MemberDao extends IService<Member> {

    /**
     * 查询用户
     *
     * @param qry
     * @return
     */
    List<MemberDTO> searchMember(MemberQry qry);
}

@Slf4j
@Repository
public class MemberDaoImpl extends ServiceImpl<MemberMapper, Member> implements MemberDao {

    @Resource
    private MemberMapper memberMapper;

    @Override
    public List<MemberDTO> searchMember(MemberQry qry) {
        return memberMapper.searchMember(qry);
    }
}

3.9 service包

public interface MemberService {

    /**
     * 通过ID查询
     *
     * @param id
     * @return
     */
    Member findById(Long id);

    /**
     * 新增用户
     *
     * @param editVO
     * @return
     */
    Boolean addMember(MemberEditVO editVO);

    /**
     * 查询用户
     *
     * @param qry
     * @return
     */
    List<MemberDTO> searchMember(MemberQry qry);
}

@Slf4j
@Service
public class MemberServiceImpl implements MemberService {

    @Resource
    private MemberDao memberDaoImpl;

    @Override
    public Member findById(Long id) {
        return Optional.ofNullable(memberDaoImpl.getById(id)).orElseThrow(() -> new RuntimeException("数据不存在"));
    }

    @Override
    public Boolean addMember(MemberEditVO editVO) {
        Member entity = MemberConvert.instance.editVoToEntity(editVO);
        return memberDaoImpl.save(entity);
    }

    @Override
    public List<MemberDTO> searchMember(MemberQry qry) {
        return memberDaoImpl.searchMember(qry);
    }
}

3.10 controller包

@Api(tags = "用户")
@RestController
@RequestMapping("member")
public class MemberController {

    @Resource
    private MemberService memberServiceImpl;

    @ApiOperation(value = "通过ID查询")
    @GetMapping("{id}")
    public Member findById(@PathVariable Long id) {
        return memberServiceImpl.findById(id);
    }

    @ApiOperation(value = "新增用户")
    @PostMapping
    public Boolean addMember(@RequestBody @Valid MemberEditVO editVO) {
        return memberServiceImpl.addMember(editVO);
    }

    @ApiOperation(value = "查询用户")
    @GetMapping("qry")
    public List<MemberDTO> searchMember(MemberQry qry) {
        return memberServiceImpl.searchMember(qry);
    }
}

3.11 Application类

package com.ac.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.ac.member.mapper")
public class ProductApplication {

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

3.12 yml项目配置文件

application.yml

spring:
  profiles:
    active: dev

application-dev.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.100.51:3306/ac_db?serverTimezone=Asia/Shanghai&useUnicode=true&tinyInt1isBit=false&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: ac_u
    password: ac_PWD_123

    #hikari数据库连接池
    hikari:
      pool-name: YH_HikariCP
      minimum-idle: 10 #最小空闲连接数量
      idle-timeout: 600000 #空闲连接存活最大时间,默认600000(10分钟)
      maximum-pool-size: 100 #连接池最大连接数,默认是10
      auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true
      max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
      connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
      connection-test-query: SELECT 1

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

swagger:
  enabled: true

3.13 项目结构截图

mall-pom、mall-member项目截图

3.14 说明

3.14.1 mapper层与dao层

在大部分项目中,代码分为3层,大概是这样的:

  • controller层:RESTful API 或 HTTP请求转发,没有业务逻辑。
  • service层:放业务逻辑代码,有些会继承MyBatisPlus的IService接口,获取通用的对象CURD方法。
  • mapper层:继承MyBatisPlus的BaseMapper,获取通用的对象CURD方法,同时对应mapper.xml里的自定义sql。

而在本项目中,代码分为4层,分别是:controller、service、dao、mapper。

service层不依赖任何第三方框架,即service不去继承MyBatisPlus的IService接口,这样就保证了service层的高内聚低耦合,将来哪一天项目要将ORM框架由MyBatisPlus换成其他的框架时,service层完全不需要改动,保障了业务逻辑代码的稳定性。

但MyBatisPlus的IService接口提供了丰富的通用CURD方法,如果不用又很浪费,因此,本项目就多加了一个dao层,dao层继承MyBatisPlus的IService接口,这样service再依赖dao,调用dao提供的CURD,而mapper层专门对应mapper.xml里的自定义sql。从广义上来讲,数据存储层(通常说的dao层)在本项目中包含两部分:dao和mapper,将来哪一天项目要将ORM框架由MyBatisPlus换成其他的框架时,只需要改造dao和mapper,且dao和mapper一般相对简单,没有业务逻辑代码,改造完成后容易测试。

3.14.2 @Autowired@Resource

在Spring项目中,IOC注入有的人用@Autowired的,而有的人用@Resource,而且写法各式各样。

3.14.2.1 我的用法

本项目中,我的写法是:

  • 1、@Autowired@Resource,我用的是@Resource,理由是@Autowired依赖Spring框架,而@Resource是Java自带注解。
  • 2、引用名字用的是接口实现类的名字,而不是接口名字。

例如,我在MemberController里写的是实现类的类名memberServiceImpl,而不是接口名memberService

理由是@Resource默认是byName查找的,如果写的是接口名memberService,IOC注入时默认byName查找,发现没有找到name为memberService,然后通过byType进行第二次查找,才找到接口类型为MemberService的类,而且只发现一个实现类MemberServiceImpl,注入成功。

即,用实现类的类名,IOC通过byName一次就能注入成功,而用接口名,需要两次查找,性能会差一些。

public class MemberController {

    @Resource
    private MemberService memberServiceImpl;

}

3.14.2.2 @Autowired@Resource的区别

A、相同点

这个两个注解都是用来完成组件的装配的,即利用依赖注入(DI),完成对IOC容器当中各个组件之间依赖的装配赋值。

B、不同点

B1、来源不同

@Resource

@Resource是javaEE的注解,它遵循的是JSR-250规范,需要导入包javax.annotation.Resource

@Autowired

@Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired

B2、装配顺序不同

@Resource

  • 默认按照byName方式进行装配,属于J2EE自带注解,没有指定name时,name指的是变量名。

  • 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。

  • 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。

  • 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常。

  • 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配。如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。

@Autowired

  • 默认按byType自动注入,是Spring的注解。
  • 默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,@Autowired(required = false)
  • 按类型装配的过程中,如果发现找到多个bean,则又按照byName方式进行比对,如果还有多个,则报出异常。

四、MyBatis-Plus配置

4.1 雪花ID生成策略

4.1.1 雪花ID生成代码

import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

@Slf4j
public class SnowFlake {

    /** 初始偏移时间戳 */
    private static final long OFFSET = 1546300800L;

    /** 机器id (0~15 保留 16~31作为备份机器) */
    private static final long WORKER_ID;
    /** 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32)*/
    private static final long WORKER_ID_BITS = 5L;
    /** 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = 65536) */
    private static final long SEQUENCE_ID_BITS = 16L;
    /** 机器id偏移位数 */
    private static final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS;
    /** 自增序列偏移位数 */
    private static final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS;
    /** 机器标识最大值 (2^5 / 2 - 1 = 15) */
    private static final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1;
    /** 备份机器ID开始位置 (2^5 / 2 = 16) */
    private static final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1;
    /** 自增序列最大值 (2^16 - 1 = 65535) */
    private static final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1;
    /** 发生时间回拨时容忍的最大回拨时间 (秒) */
    private static final long BACK_TIME_MAX = 1000L;

    /** 上次生成ID的时间戳 (秒) */
    private static long lastTimestamp = 0L;
    /** 当前秒内序列 (2^16)*/
    private static long sequence = 0L;
    /** 备份机器上次生成ID的时间戳 (秒) */
    private static long lastTimestampBak = 0L;
    /** 备份机器当前秒内序列 (2^16)*/
    private static long sequenceBak = 0L;

    static {
        // 初始化机器ID
        long workerId = getWorkId();
        if (workerId < 0 || workerId > WORKER_ID_MAX) {
            throw new IllegalArgumentException(String.format("cmallshop.workerId范围: 0 ~ %d 目前: %d", WORKER_ID_MAX, workerId));
        }
        WORKER_ID = workerId;
    }

    private static Long getWorkId(){
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for(int b : ints){
                sums += b;
            }
            return (long)(sums % WORKER_ID_MAX);
        } catch (UnknownHostException e) {
            // 如果获取失败,则使用随机数备用
            return RandomUtils.nextLong(0,WORKER_ID_MAX-1);
        }
    }


    /** 私有构造函数禁止外部访问 */
    private SnowFlake() {}

    /**
     * 获取自增序列
     * @return long
     */
    public static long nextId() {
        return nextId(SystemClock.now() / 1000);
    }

    /**
     * 主机器自增序列
     * @param timestamp 当前Unix时间戳
     * @return long
     */
    private static synchronized long nextId(long timestamp) {
        // 时钟回拨检查
        if (timestamp < lastTimestamp) {
            // 发生时钟回拨
            log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp);
            return nextIdBackup(timestamp);
        }

        // 开始下一秒
        if (timestamp != lastTimestamp) {
            lastTimestamp = timestamp;
            sequence = 0L;
        }
        if (0L == (++sequence & SEQUENCE_MAX)) {
            // 秒内序列用尽
//            log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp);
            sequence--;
            return nextIdBackup(timestamp);
        }

        return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence;
    }

    /**
     * 备份机器自增序列
     * @param timestamp timestamp 当前Unix时间戳
     * @return long
     */
    private static long nextIdBackup(long timestamp) {
        if (timestamp < lastTimestampBak) {
            if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) {
                timestamp = lastTimestampBak;
            } else {
                throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak));
            }
        }

        if (timestamp != lastTimestampBak) {
            lastTimestampBak = timestamp;
            sequenceBak = 0L;
        }

        if (0L == (++sequenceBak & SEQUENCE_MAX)) {
            // 秒内序列用尽
//            logger.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp);
            return nextIdBackup(timestamp + 1);
        }

        return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak;
    }


    /**
     * 并发数
     */
    private static final int THREAD_NUM = 30000;
    private static volatile CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);

    public static void main(String[] args) {
        ConcurrentHashMap<Long,Long> map = new ConcurrentHashMap<>(THREAD_NUM);
        List<Long> list = Collections.synchronizedList(new LinkedList<>());

        for (int i = 0; i < THREAD_NUM; i++) {
            Thread thread = new Thread(() -> {
                // 所有的线程在这里等待
                try {
                    countDownLatch.await();

                    Long id = SnowFlake.nextId();
                    list.add(id);
                    map.put(id,1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

            thread.start();
            // 启动后,倒计时计数器减一,代表有一个线程准备就绪了
            countDownLatch.countDown();
        }

        try{
            Thread.sleep(50000);
        }catch (Exception e){
            e.printStackTrace();
        }

        System.out.println("listSize:"+list.size());
        System.out.println("mapSize:"+map.size());
        System.out.println(map.size() == THREAD_NUM);
    }
}

4.1.2 与mybatis-plus结合

@Slf4j
@Component
public class CustomIdGenerator implements IdentifierGenerator {

    @Override
    public Long nextId(Object entity) {
        return SnowFlake.nextId();
    }
}

4.2 自动填充功能

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * @description 处理时间自动填充
 */
@Slf4j
@Component
public class GlobalMetaObjectHandler implements MetaObjectHandler {

    private Logger logger = LoggerFactory.getLogger(GlobalMetaObjectHandler.class);

    String createUserIdFieldName = "createUserId";
    String updateUserIdFieldName = "updateUserId";
    String createTimeFieldName = "createTime";
    String updateTimeFieldName = "updateTime";
    String deletedFieldName = "deleted";

    @Override
    public void insertFill(MetaObject metaObject) {
        try {
            Object createUserId = getFieldValByName(createUserIdFieldName, metaObject);
            Object createTime = getFieldValByName(createTimeFieldName, metaObject);
            Object updateTime = getFieldValByName(updateTimeFieldName, metaObject);
            Object delTag = getFieldValByName(deletedFieldName, metaObject);

            if(null ==createUserId){
                setFieldValByName(createUserIdFieldName, getUserId(), metaObject);
            }

            LocalDateTime date = LocalDateTime.now();
            if (null == createTime) {
                setFieldValByName(createTimeFieldName, date, metaObject);
            }
            if (null == updateTime) {
                setFieldValByName(updateTimeFieldName, date, metaObject);
            }
            if (null == delTag) {
                setFieldValByName(deletedFieldName, false, metaObject);
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        try {
            setFieldValByName(updateTimeFieldName, LocalDateTime.now(), metaObject);
            setFieldValByName(updateUserIdFieldName, getUserId(), metaObject);
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
    }

    private String getUserId(){
        String userId = null;
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if(requestAttributes !=null){
            HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
            if(request!=null){
                userId = request.getHeader("userId");
            }
        }
        return userId;
    }
}

4.3 MyBatis配置类截图

五、Swagger配置

5.1 Swagger依赖包

Swagger依赖的包已经在mall-pom项目的pom.xml配置,如下

<!--swagger2 start-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>${swagger2.version}</version>
</dependency>

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>${swagger2.version}</version>
</dependency>
<!--swagger2 end-->

5.2 Swagger配置

package com.ac.member.config.swagger;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
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.EnableSwagger2;

/**
 * @description Swagger文档配置
 */
@EnableWebMvc
@Configuration
@EnableSwagger2
public class SwaggerConfig implements WebMvcConfigurer {

    //是否开启swagger,生产环境一般关闭
    @Value("${swagger.enabled}")
    private boolean enabled;

    /**
     * @return
     */
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enabled)
                .apiInfo(apiInfo())
                .select()
                //.apis(RequestHandlerSelectors.any())
                .apis(RequestHandlerSelectors.basePackage("com.ac.member.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 一些接口文档信息的简介
     *
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("用户服务")
                .description("用户服务相关接口")
                .termsOfServiceUrl("")
                .version("1.0")
                .build();
    }

    /**
     * swagger ui资源映射
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * swagger-ui.html路径映射,浏览器中使用/api-docs访问
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/api-docs", "/swagger-ui.html");
    }
}

5.3 Swagger访问效果

在浏览器中输入地址http://127.0.0.1:8080/swagger-ui.html 或地址http://127.0.0.1:8080/api-docs,效果如下:

5.4 Swagger访问404问题

5.4.1 404效果图

5.4.2 原因

由于要处理日期返回到前端格式化的问题,项目里加了一个处理日期格式化的配置类,继承了WebMvcConfigurationSupport类,导致swagger访问404

@Configuration
public class DateJsonConfig extends WebMvcConfigurationSupport {
        //代码省略
}

5.4.3 解决方案

将继承WebMvcConfigurationSupport类,改成实现WebMvcConfigurer接口,代码如下:

@Configuration
public class DateJsonConfig implements WebMvcConfigurer {
     //代码省略
}

六、返回JSON数据日期格式化

6.1 未处理前日期返回的格式

6.2 处理日期格式配置类

package com.ac.member.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;

@Configuration
public class DateJsonConfig implements WebMvcConfigurer {

    /**
     * 日期时间格式 yyyy-MM-dd HH:mm:ss
     */
    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

    /**
     * 日期格式 yyyy-MM-dd
     */
    private static final String DATE_FORMAT = "yyyy-MM-dd";

    /**
     * 时间格式 HH:mm:ss
     */
    private static final String TIME_FORMAT = "HH:mm:ss";

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = converter.getObjectMapper();

        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

        JavaTimeModule javaTimeModule = new JavaTimeModule();

        //LocalDateTime的序列化和反序列化
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));

        //LocalDate的序列化和反序列化
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));

        //LocalTime的序列化和反序列化
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));

        //Date类型的序列化
        javaTimeModule.addSerializer(Date.class, new JsonSerializer<Date>() {
            @Override
            public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                SimpleDateFormat formatter = new SimpleDateFormat(DATE_TIME_FORMAT);
                String formattedDate = formatter.format(date);
                jsonGenerator.writeString(formattedDate);
            }
        });

        //Date类型的反序列化
        javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() {
            @Override
            public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
                SimpleDateFormat format = new SimpleDateFormat(DATE_TIME_FORMAT);
                String date = jsonParser.getText();
                try {
                    return format.parse(date);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        objectMapper.registerModule(javaTimeModule);
        converter.setObjectMapper(objectMapper);
        converters.add(0, converter);
    }
}

日期格式返回效果

七、创建mall-common服务工程

各微服务虽然是独立的,但一般都会有一些公用重复的功能,可以把这些重复的代码提取到mall-common服务,然后其他微服务依赖mall-common。

7.1 创建module

7.2 pom.xml配置

参考上面的mall-member服务的pom.xml来写mall-common服务的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>

    <parent>
        <artifactId>mall-pom</artifactId>
        <groupId>com.ac</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <groupId>com.ac</groupId>
    <artifactId>mall-common</artifactId>
    <version>${mall.version}</version>
    <name>mall-common</name>
    <description>公共服务</description>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

7.3 提取功能config包

将mall-member服务里的config包提取到mall-common服务中,并将mall-member服务里的config包删除掉。

删除config包

mall-common包结构

7.4 mall-member服务依赖mall-common服务

让mall-member服务里的Member实体类继承mall-common服务里的BaseEntity类,并删除deleted、createTime、updateTime字段。

根据IDEA提示:Add dependency on module mall-common,IDEA会帮我们自动创建依赖

Add dependency on module

IDEA自动创建依赖

我们可以手动把<scope>compile</scope> 删除。

说明:后面我将mall-common里的内容放到了一个新服务mall-core,我对mall-core的定义是:稳定的不经常改动的基础服务。而mall-common里则放一些各个服务都需要依赖的一些公共类,如全局常量(topic name)、枚举等,该服务经常会被改动。

7.5 配置@ComponentScan

@SpringApplication注解里已经包含了@ComponentScan注解,但@ComponentScan注解默认扫描的是Application所在目录下的所有Bean,但mall-common里的配置类显然不在mall-member服务的Application所在目录下,导致mall-common里的配置类没有生效,因此我们需要修改mall-member服务的Application配置,扫描mall-common服务里的Bean。需要添加的配置为:@ComponentScan("com.ac.*"),MemberApplication完整代码如下:

package com.ac.member;

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

@SpringBootApplication
@MapperScan("com.ac.member.mapper")
@ComponentScan("com.ac.*")
public class MemberApplication {

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

八、创建mall-product服务工程

mall-product 服务主要维护产品数据,可参考mall-member来建该服务。

创建mall-product
mall-product项目截图

九、Swagger优化

9.1 mall-common支持多个微服务Swagger配置

Swagger配置类SwaggerConfig由原来的mall-member移到了mall-common服务,因此之前在SwaggerConfig写死的配置信息需要放到yml文件进行动态配置。

9.1.1 修改代码

修改后的SwaggerConfig如下:

package com.ac.common.config.swagger;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
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.EnableSwagger2;

/**
 * @description Swagger文档配置
 */
@EnableWebMvc
@Configuration
@EnableSwagger2
@ConfigurationProperties("swagger")
@Data
public class SwaggerConfig implements WebMvcConfigurer {

    //取yml配置文件参数
    private boolean enabled;
    private String title;
    private String description;
    private String version;
    private String basePackage;

    /**
     * @return
     */
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enabled)
                .apiInfo(apiInfo())
                .select()
                //.apis(RequestHandlerSelectors.any())
                //.apis(RequestHandlerSelectors.basePackage(basePackage))
                .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 一些接口文档信息的简介
     *
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(title)
                .description(description)
                .termsOfServiceUrl("")
                .version(version)
                .build();
    }

    /**
     * swagger ui资源映射
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * swagger-ui.html路径映射,浏览器中使用/api-docs访问
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/api-docs", "/swagger-ui.html");
    }
}

修改mall-member服务的yml配置文件

swagger:
  enabled: true
  title: 用户服务
  basePackage: com.ac.member.controller
  version: 1.0
  description: 用户服务相关接口

修改mall-product服务的yml配置文件

swagger:
  enabled: true
  title: 产品服务
  basePackage: com.ac.product.controller
  version: 1.0
  description: 产品服务相关接口

9.1.2 swagger访问效果

用户服务

产品服务

9.2 Swagger传参

9.2.1 修改代码

修改SwaggerConfig配置类,让Swagger支持token、语言、测试账号参数

SwaggerConfig类

package com.ac.common.config.swagger;

import com.ac.common.enums.LanguageEnum;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.AllowableListValues;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;


@EnableWebMvc
@Configuration
@EnableSwagger2
@ConfigurationProperties("swagger")
@Data
public class SwaggerConfig implements WebMvcConfigurer {

    //取yml配置文件参数
    private boolean enabled;
    private String title;
    private String description;
    private String version;
    private String basePackage;

    /**
     * @return
     */
    @Bean
    public Docket api() {
        List<Parameter> pars = new ArrayList<>();
        pars.add(buildToken().build());
        pars.add(buildLang().build());
        pars.add(buildDemos().build());

        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enabled)
                .apiInfo(apiInfo())
                .directModelSubstitute(LocalDateTime.class, String.class)
                .directModelSubstitute(LocalDate.class, String.class)
                .directModelSubstitute(LocalTime.class, String.class)
                .directModelSubstitute(Byte.class, Integer.class)
                .select()
                //.apis(RequestHandlerSelectors.any())
                //.apis(RequestHandlerSelectors.basePackage(basePackage))
                .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
                .paths(PathSelectors.any())
                .build()
                .globalOperationParameters(pars);
    }

    /**
     * 一些接口文档信息的简介
     *
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(title)
                .description(description)
                .termsOfServiceUrl("")
                .version(version)
                .build();
    }

    private ParameterBuilder buildToken() {
        ParameterBuilder pb = new ParameterBuilder();
        pb.name("Authorization")
                .description("授权的token, 格式: Bearer xxx(需通过网关)")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false)
                .build(); //header中的ticket参数非必填,传空也可以
        return pb;
    }

    private ParameterBuilder buildLang() {
        List<String> languages = new ArrayList<>();
        languages.add(LanguageEnum.zh_CN.getCode());
        languages.add(LanguageEnum.zh_HK.getCode());
        languages.add(LanguageEnum.en_US.getCode());
        ParameterBuilder pb = new ParameterBuilder();
        pb.name("lang")
                .description("当前国际化语言环境")
                .modelRef(new ModelRef("string"))
                .allowableValues(new AllowableListValues(languages, "string"))
                .parameterType("header")
                .required(false)
                .build(); //header中的ticket参数非必填,传空也可以
        return pb;
    }

    private ParameterBuilder buildDemos() {
        List<String> demouis = new ArrayList<>();
        demouis.add("test01");
        demouis.add("test02");
        ParameterBuilder pb = new ParameterBuilder();
        pb.name("Swagger测试用户")
                .description("Swagger模拟用户, 选中时Authorization字段将失效(无需通过网关)")
                .modelRef(new ModelRef("string"))
                .allowableValues(new AllowableListValues(demouis, "string"))
                .parameterType("header")
                .required(false)
                .order(0)
                .build(); //header中的ticket参数非必填,传空也可以
        return pb;
    }

    /**
     * swagger ui资源映射
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * swagger-ui.html路径映射,浏览器中使用/api-docs访问
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/api-docs", "/swagger-ui.html");
    }
}

LanguageEnum类

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "多语言类型枚举")
public enum LanguageEnum {
    zh_CN("zh_CN", "简体中文", new Locale("zh", "CN")),
    zh_HK("zh_HK", "繁體中文", new Locale("zh", "HK")),
    en_US("en_US", "English", new Locale("en", "US"));

    /**
     * 语言代码
     */
    private String code;
    /**
     * 语言名称
     */
    private String name;
    /**
     * Locale
     */
    private Locale locale;

    /**
     * 解析
     *
     * @param type
     * @return
     */
    public static LanguageEnum parse(String type) {
        return parse(type, null);
    }

    /**
     * 解析
     *
     * @param type
     * @param dau
     * @return
     */
    public static LanguageEnum parse(String type, LanguageEnum dau) {
        if (null != type && !type.isEmpty()) {
            try {
                return LanguageEnum.valueOf(type);
            } catch (IllegalArgumentException e) {
            }
        }
        return dau;
    }
}

9.2.2 Swagger参数效果图

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/551501.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【CH32】| 00——开发环境搭建 | 软件安装 | 资料及工具下载

系列文章目录 TODO 文章目录 1. 简介2. 下载软件3. 安装4. 资料及工具下载4.1 芯片数据手册/参考手册4.2 评估(开发)板原理图/demo4.3 烧录软件4.3.1 isp串口一键下载4.3.2 WCH-LINK Utility烧录 4.4 WCH LINK相关资料/驱动安装4.4.1 WCH LINK相关资料4.4.2 WCH LINK驱动安装 5…

摆摊卖网红气球怎么样?

本章主要介绍一下最近网红气球&#xff1a; 最近看到很多摆摊的抖音视频&#xff0c;都在说卖气球很好&#xff0c;成本低&#xff0c;收益高&#xff0c;所以调研了一下&#xff0c;网红气球分好几种&#xff1a; a,飘空气球&#xff1b; b.手持网红气球 c.青蛙 首先介绍飘空…

初始python

初始python 缘由 ​ 对于python&#xff0c;相比大家并不陌生&#xff0c;Python是一种高级的、解释性编程语言&#xff0c;它具有简洁的语法和强大的内置函数&#xff0c;适用于广泛的应用领域&#xff0c;例如运维,Web开发&#xff0c;数据科学&#xff0c;人工智能等。Pyt…

JVM基础学习---1、JVM总体机制、类加载机制

1、JVM总体机制 1.1 JVM概念 JVM&#xff1a;Java Virtual Machine&#xff0c;翻译过来是Java虚拟机。 JRE&#xff1a;Java Runtime Environment&#xff0c;翻译过来是Java运行时环境。 JDK&#xff1a;Java Development Ki JDK&#xff1a;Java Development Kits&#…

Mysql字符集

1、修改MySQL5.7字符集 我们在新建数据库时&#xff08;Navicat&#xff09;界面时会遇到这两个选项&#xff0c;字符集和排序规则 在MySQL 8.0版本之前&#xff0c;MySQL 5.7 默认的客户端和服务器都用了 latin1 &#xff0c;而latin1是不包含中文的&#xff0c;所以保存中文…

MetersPhere 试用

1. 功能测试 1.1 用例管理 脑图管理 样式&#xff1a; 脑图用例导入 1.2 用例评审 1.2.1 发起用例评审 1.2.2 关联用例&#xff1a; 1.2.3 用例评审 2. 接口测试 2.1 接口定义 类似postman、foxapi等主流接口管理页面 2.2 用例配置&#xff1a; 需要在meterphere安装节点…

路径规划算法:基于人工蜂群算法的路径规划算法- 附代码

路径规划算法&#xff1a;基于人工蜂群的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于人工蜂群的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法人工…

C++ [STL之vector的使用]

本文已收录至《C语言和高级数据结构》专栏&#xff01; 作者&#xff1a;ARMCSKGT STL之vector的使用 前言正文默认成员函数普通构造拷贝构造析构函数赋值重载 迭代器正向迭代器反向迭代器const迭代器 容量类空间容量查询空间容量操作扩容操作元素数量操作缩容操作 数据访问下标…

numpy log随机产生非常奇怪的数字(np.log的大坑)

背景 有一批信号数据要送到网络里训练&#xff0c;训练之前为了统一量纲&#xff0c;首先根据方差和均值做了一次标准化&#xff0c;然后求了一次能量&#xff08;20*log10(x)&#xff09;&#xff0c;也就是说送进网络里的其实是一个能量谱&#xff0c;但是训练过程中经常蹦出…

Python的用途与学习计划

python的用途&#xff1a; 1、web开发&#xff1b; 2、网络爬虫&#xff1b; 3、数据科学&#xff1b; 4、自动化运维&#xff1b; 5、数据库编程&#xff1b; 6、网络编程&#xff1b; 7、图形处理、数学处理、文本处理&#xff1b; 8、多媒体应用。 其中&#xff0c…

组合问题-回溯算法

1题目 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ] 示例 2&#xff1a; 输入&#xff1a;n …

美国国际留学生超136万,本科、硕士和博士各占多少?

国际教育市场研究机构ICEF Monitor近日公布&#xff0c;2022年美国持有F-1和M-1有效学习签证的国际学生数量共计136.2万人&#xff0c;与2021年相比增长了10.1%。 其中&#xff0c;国际学生来美国读本科学士学位的占37%&#xff0c;读硕士学位的占41%&#xff0c;读博士学位的…

为什么WordPress这么难用?(以及如何让它变得简单点)

WordPress 是世界上最受欢迎的网站构建器&#xff0c;为互联网上超过 43% 的网站提供支持。然而&#xff0c;有些人抱怨说 WordPress 比 Squarespace 和 Wix 等解决方案更难使用。 在本文中&#xff0c;我们将解决为何WordPress这么难用的神话&#xff0c;并分享您可以用来毫无…

用于具有缺失模态的脑肿瘤分割的模态自适应特征交互

文章目录 Modality-Adaptive Feature Interaction for Brain Tumor Segmentation with Missing Modalities摘要本文方法Modality-Adaptive Feature InteractionGraph RepresentationGraph Edge ComputationModality Feature Updating 实验结果 Modality-Adaptive Feature Inter…

OpenCV入门-基于Python

图像入门 1. 创建窗口namedWindow()resizeWindow()destroyAllWindow() 2.显示图像imread()imshow()imwrite()waitKey()flip() 代码演示3.显示视频VideoCapure()对象cap.get()cap.isOpened()cap.read()cap.release() 部分功能代码演示VideoWriter()对象VideoWriter_fourcc()writ…

K8s之Pod生命周期、启动停止钩子

文章目录 一、Pod生命周期流程二、初始化容器-initContainers三、主容器操作-containers1、启动钩子-lifecycle.postStart2、停止钩子-lifecycle.preStop 一、Pod生命周期流程 Pod生命周期整个过程 如下图&#xff1a; 1、在启动任何容器之前&#xff0c;前创建 pause 容器&am…

u-boot移植:详细讲解移植u-boot.2022.10版本到imx6ull开发板

目录 一、u-boot编译环境准备 1.安装交叉编译工具链 2.u-boot源码包下载 3.编译 4.安装依赖库 二、U-Boot中添加自己的开发板 1、添加开发板默认配置文件 2、添加开发板对应的头文件 3、添加开发板对应的板级文件夹 4、修改 arch/arm/mach-imx/mx6/Kconfig 5、其他需…

5分钟使用UNI-APP框架创建你的第一个项目

UNI-APP学习系列 5分钟使用UNI-APP框架创建你的第一个项目 文章目录 UNI-APP学习系列前言uni-app框架创建项目一、HBuilderX可视化方式二、 vue-cli命令行方式 总结 前言 UNI-APP学习系列之5分钟创建自己的第一个uni-app项目。 uni-app框架创建项目 创建方式 一、HBuilderX可…

bash: /opt/ros/kinetic/setup.bash: 没有那个文件或目录

有时候打开终端的时候&#xff0c;会在第一行报错&#xff1a; bash: /opt/ros/kinetic/setup.bash: 没有那个文件或目录 bash: /opt/ros/melodic/setup.bash: 没有那个文件或目录 bash: /opt/ros/neodic/setup.bash: 没有那个文件或目录 凡是类似报错与bash有关的&#xf…

【PCIE703】XCKU060+海思视频处理器HI3531DV200-ARM的高性能综合视频图像处理平台设计资料及调试经验

板卡概述 PCIE703是我司自主研制的一款基于PCIE总线架构的高性能综合视频图像处理平台&#xff0c;该平台采用Xilinx的高性能Kintex UltraScale系列FPGA加上华为海思的高性能视频处理器来实现。 华为海思的HI3531DV200是一款集成了ARM A53四核处理器性能强大的神经网络引擎&am…