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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 本节实现目标搭建ac-mall2-cloud微服务基础骨架。搭建微服务子项目:mall-pom、mall-common、mall-member、mall-product。MyBatis-Plus配置:雪花ID、创建时间/修改时间 自动填充。单个微服务子项目Swagger配置及访问。返回JSON数据日期格式化。Swagger优化:mall-common支持多个微服务Swagger配置、Swagger传参(语言参数、token、测试账号)

 

总概

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,该目录并列存放所有微服务。

      image.gif编辑

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

      2.1 创建项目

      image.gif编辑

      2.2 选择maven项目

      image.gif编辑

      2.3 项目名称和路径

      image.gif编辑

      2.4 配置.gitignore文件

      .idea
      target
      *.iml

      image.gif

      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>

      image.gif

      2.6 modules问题

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

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

      image.gif

      替换成:

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

      image.gif

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

      3.1 创建module

      image.gif编辑

      3.2 maven项目

      image.gif编辑

      3.3 项目名称和路径

      image.gif编辑

      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>

      image.gif

      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;
      }

      image.gif

      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);
      }

      image.gif

      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>

      image.gif

      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);
      }

      image.gif

      @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);
          }
      }

      image.gif

      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);
      }

      image.gif

      @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);
          }
      }

      image.gif

      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);
          }
      }

      image.gif

      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);
          }
      }

      image.gif

      3.12 yml项目配置文件

      application.yml

      spring:
        profiles:
          active: dev

      image.gif

      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

      image.gif

      3.13 项目结构截图

      mall-pom、mall-member项目截图

      image.gif编辑

      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;
          }

          image.gif

          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);
                  }
              }

              image.gif

              4.1.2 与mybatis-plus结合

              @Slf4j
              @Component
              public class CustomIdGenerator implements IdentifierGenerator {
                  @Override
                  public Long nextId(Object entity) {
                      return SnowFlake.nextId();
                  }
              }

              image.gif

              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;
                  }
              }

              image.gif

              4.3 MyBatis配置类截图

              image.gif编辑

              五、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-->

              image.gif

              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");
                  }
              }

              image.gif

              5.3 Swagger访问效果

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

              image.gif编辑

              5.4 Swagger访问404问题

              5.4.1 404效果图

              image.gif编辑

              5.4.2 原因

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

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

              image.gif

              5.4.3 解决方案

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

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

              image.gif

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

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

              image.gif编辑

              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);
                  }
              }

              image.gif

              日期格式返回效果

              image.gif编辑

              七、创建mall-common服务工程

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

              7.1 创建module

              image.gif编辑

              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>

              image.gif

              7.3 提取功能config包

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

              删除config包image.gif编辑

              mall-common包结构image.gif编辑

              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 moduleimage.gif编辑

              IDEA自动创建依赖image.gif编辑

              我们可以手动把<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);
                  }
              }

              image.gif

              八、创建mall-product服务工程

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

              创建mall-productimage.gif编辑

              mall-product项目截图image.gif编辑

              九、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");
                  }
              }

              image.gif

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

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

              image.gif

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

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

              image.gif

              9.1.2 swagger访问效果

              image.gif编辑

              用户服务

              image.gif编辑

              产品服务

              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");
                  }
              }

              image.gif

              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;
                  }
              }

              image.gif

              9.2.2 Swagger参数效果图

              image.gif编辑

              相关实践学习
              如何在云端创建MySQL数据库
              开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
              全面了解阿里云能为你做什么
              阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
              相关文章
              |
              1月前
              |
              API 持续交付 开发者
              后端开发中的微服务架构实践与挑战
              在数字化时代,后端服务的构建和管理变得日益复杂。本文将深入探讨微服务架构在后端开发中的应用,分析其在提高系统可扩展性、灵活性和可维护性方面的优势,同时讨论实施微服务时面临的挑战,如服务拆分、数据一致性和部署复杂性等。通过实际案例分析,本文旨在为开发者提供微服务架构的实用见解和解决策略。
              |
              2月前
              |
              Java 持续交付 微服务
              后端开发中的微服务架构实践与挑战####
              本文深入探讨了微服务架构在现代后端开发中的应用,通过具体案例分析,揭示了其如何助力企业应对业务复杂性、提升系统可维护性和可扩展性。文章首先概述了微服务的核心概念及其优势,随后详细阐述了实施微服务过程中的关键技术选型、服务拆分策略、容错机制以及持续集成/持续部署(CI/CD)的最佳实践。最后,通过一个真实世界的应用实例,展示了微服务架构在实际项目中的成功应用及其带来的显著成效。 ####
              |
              2月前
              |
              XML JSON API
              ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
              【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
              138 3
              |
              2月前
              |
              设计模式 API 开发者
              探索现代后端开发:微服务架构与API设计
              【10月更文挑战第6天】探索现代后端开发:微服务架构与API设计
              |
              7天前
              |
              运维 监控 Java
              后端开发中的微服务架构实践与挑战####
              在数字化转型加速的今天,微服务架构凭借其高度的灵活性、可扩展性和可维护性,成为众多企业后端系统构建的首选方案。本文深入探讨了微服务架构的核心概念、实施步骤、关键技术考量以及面临的主要挑战,旨在为开发者提供一份实用的实践指南。通过案例分析,揭示微服务在实际项目中的应用效果,并针对常见问题提出解决策略,帮助读者更好地理解和应对微服务架构带来的复杂性与机遇。 ####
              |
              27天前
              |
              监控 API 持续交付
              后端开发中的微服务架构实践与挑战####
              本文深入探讨了微服务架构在后端开发中的应用,分析了其优势、面临的挑战以及最佳实践策略。不同于传统的单体应用,微服务通过细粒度的服务划分促进了系统的可维护性、可扩展性和敏捷性。文章首先概述了微服务的核心概念及其与传统架构的区别,随后详细阐述了构建微服务时需考虑的关键技术要素,如服务发现、API网关、容器化部署及持续集成/持续部署(CI/CD)流程。此外,还讨论了微服务实施过程中常见的问题,如服务间通信复杂度增加、数据一致性保障等,并提供了相应的解决方案和优化建议。总之,本文旨在为开发者提供一份关于如何在现代后端系统中有效采用和优化微服务架构的实用指南。 ####
              |
              29天前
              |
              消息中间件 设计模式 运维
              后端开发中的微服务架构实践与挑战####
              本文深入探讨了微服务架构在现代后端开发中的应用,通过实际案例分析,揭示了其在提升系统灵活性、可扩展性及促进技术创新方面的显著优势。同时,文章也未回避微服务实施过程中面临的挑战,如服务间通信复杂性、数据一致性保障及部署运维难度增加等问题,并基于实践经验提出了一系列应对策略,为开发者在构建高效、稳定的微服务平台时提供有价值的参考。 ####
              |
              1月前
              |
              消息中间件 监控 数据管理
              后端开发中的微服务架构实践与挑战####
              【10月更文挑战第29天】 在当今快速发展的软件开发领域,微服务架构已成为构建高效、可扩展和易于维护应用程序的首选方案。本文探讨了微服务架构的核心概念、实施策略以及面临的主要挑战,旨在为开发者提供一份实用的指南,帮助他们在项目中成功应用微服务架构。通过具体案例分析,我们将深入了解如何克服服务划分、数据管理、通信机制等关键问题,以实现系统的高可用性和高性能。 --- ###
              39 2
              |
              1月前
              |
              缓存 运维 监控
              后端开发中的微服务架构实践与挑战#### 一、
              【10月更文挑战第22天】 本文探讨了微服务架构在后端开发中的应用实践,深入剖析了其核心优势、常见挑战及应对策略。传统后端架构难以满足快速迭代与高可用性需求,而微服务通过服务拆分与独立部署,显著提升了系统的灵活性和可维护性。文章指出,实施微服务需关注服务划分的合理性、通信机制的选择及数据一致性等问题。以电商系统为例,详细阐述了微服务改造过程,包括用户、订单、商品等服务的拆分与交互。最终强调,微服务虽优势明显,但落地需谨慎规划,持续优化。 #### 二、
              |
              2月前
              |
              监控 API 开发者
              后端开发中的微服务架构实践与优化
              【10月更文挑战第17天】 本文深入探讨了微服务架构在后端开发中的应用及其优化策略。通过分析微服务的核心理念、设计原则及实际案例,揭示了如何构建高效、可扩展的微服务系统。文章强调了微服务架构对于提升系统灵活性、降低耦合度的重要性,并提供了实用的优化建议,帮助开发者更好地应对复杂业务场景下的挑战。
              24 7