(九)Alian 的 Spring Cloud 公共 API 的(API核心starter)

纪俊良
2023-12-01

一、背景

  在我之前的文章中,我编写了一个db starter,所有需要进行数据库操作的系统引用相关依赖即可,可以避免重复造轮子,但是系统还有很多的功能,比如接口文档,公共结果返回,全局异常处理,分页查询处理等,不可能每个系统都把配置依赖都拷贝一遍吧,我们也弄一个starter,可以满足我们上面的要求。

二、maven依赖

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>
        <groupId>cn.alian.microservice</groupId>
        <artifactId>parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

	<groupId>cn.alian.microservice</groupId>
    <artifactId>common-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>

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

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

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

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

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

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.github.dozermapper</groupId>
            <artifactId>dozer-core</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.dozermapper</groupId>
            <artifactId>dozer-spring-boot-starter</artifactId>
        </dependency>

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

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

        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.alian.microservice</groupId>
            <artifactId>common-db</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <inherited>false</inherited>
                <executions>
                    <execution>
                        <id>spring-boot-repackage</id>
                        <phase>none</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

三、主要

3.1 自动配置类

Config.java

package cn.alian.microservice.common.config;

import com.github.dozermapper.core.DozerBeanMapperBuilder;
import com.github.dozermapper.core.Mapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan({"cn.alian.microservice.common"})
public class Config {

    @Bean
    @ConditionalOnMissingBean
    public Mapper mapper() {
        return DozerBeanMapperBuilder.buildDefault();
    }
}

  主要是扫描我们这个包 cn.alian.microservice.common

3.2 属性配置类

BaseProperties.java

package cn.alian.microservice.common.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "app")
public class BaseProperties {

    private String gateway;

    @Value("${spring.application.name}")
    private String appName;

}

  我们自定义的属性配置类继承这个类就可以拿到公共的网关地址和应用名。比如:

@Data
@Component
@RefreshScope
@ConfigurationProperties(prefix = "app")
public class AppProperties extends BaseProperties {

}

3.3 swagger整合

SwaggerConfiguration.java

package cn.alian.microservice.common.config;

import com.google.common.base.Predicate;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StopWatch;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.StringVendorExtension;
import springfox.documentation.service.VendorExtension;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;

@Slf4j
@Configuration
@EnableSwagger2
@ComponentScan({"cn.alian.microservice.common"})
public class SwaggerConfiguration implements WebMvcConfigurer {

    @Value("${spring.application.name:app}")
    private String appName;

    @Value("${app.cors.enable:false}")
    private boolean enableCors;

    @Value("${app.swagger.enabled:false}")
    private boolean swaggerEnabled;

    @Value("${app.swagger.base.url:}")
    private String swaggerBaseUrl;

    @Autowired
    private Environment env;

    @Bean
    @ConditionalOnMissingBean
    public Predicate<RequestHandler> apiSelector() {
        //方法上标注了@ApiOperation就生成文档
        return RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class);
    }

    @Bean
    public Docket swaggerSpringFoxDocket(Predicate<RequestHandler> apiSelector) {
        if (!swaggerEnabled) {
            log.info("文档生成功能未启用");
            return new Docket(DocumentationType.SWAGGER_2).enable(false);
        }
        if ("app".equals(appName)) {
            log.warn("未设置 spring.application.name,建议在配置文件中设置");
        }
        log.info("开始生成文档");
        LocalDateTime startTime = LocalDateTime.now();

        String version = null;
        String description = "Api Documentation";
        List<VendorExtension> extensions = new ArrayList<>();
        try {
            ClassLoader classLoader = this.getClass().getClassLoader();
            InputStream inputStream = classLoader.getResourceAsStream("build.properties");
            String buildPack = null;
            if (inputStream == null) {
                log.info("未找到build.properties");
            } else {
                Properties properties = new Properties();
                //避免乱码
                properties.load(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                extensions.add(new StringVendorExtension("groupId", properties.getProperty("build.groupId")));
                extensions.add(new StringVendorExtension("artifactId", properties.getProperty("build.artifactId")));
                extensions.add(new StringVendorExtension("appName", appName));//外部经网关调用时basePath添加appName
                version = properties.getProperty("build.version");
                if (version.contains("$")) {//非打包运行
                    version = null;
                }
                String desc = properties.getProperty("build.description");
                if (StringUtils.isNotBlank(desc) && !desc.contains("$")) {
                    description = desc;
                }
                buildPack = properties.getProperty("build.pack");
            }

            String pack = null;
            String manifest = "META-INF/MANIFEST.MF";
            Enumeration<URL> resources = classLoader.getResources(manifest);
            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                InputStream stream = url.openStream();
                Properties prop = new Properties();
                prop.load(stream);
                if ("true".equals(prop.getProperty("pack"))) {
                    String startClass = prop.getProperty("Start-Class");
                    pack = startClass.substring(0, startClass.lastIndexOf("."));
                    break;
                }
            }
            if (StringUtils.isBlank(pack)) {
                if (StringUtils.isNotBlank(buildPack)) {
                    log.info("buildPack is {}", buildPack);
                    extensions.add(new StringVendorExtension("pack", buildPack));
                }
            } else {
                log.info("pack is {}", pack);
                extensions.add(new StringVendorExtension("pack", pack));
            }
        } catch (Exception e) {
            log.error(null, e);
        }
        // 文档描述信息
        ApiInfo apiInfo = new ApiInfo(appName, description,
                StringUtils.isBlank(version) ? DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()) : version,
                "urn:tos",
                ApiInfo.DEFAULT_CONTACT,
                "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", extensions);
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .enable(swaggerEnabled)
                .genericModelSubstitutes(ResponseEntity.class)
                .forCodeGeneration(true)
                .genericModelSubstitutes(ResponseEntity.class)
                .directModelSubstitute(LocalDate.class, String.class)
                .directModelSubstitute(ZonedDateTime.class, Date.class)
                .directModelSubstitute(LocalDateTime.class, Date.class)
                .select()
                .apis(apiSelector)
                .build();
        if (!StringUtils.containsAny(swaggerBaseUrl, "localhost", "127.0.0.1")) {
            //非本地则走网关
            docket.host(swaggerBaseUrl + "/" + appName);
        }
        LocalDateTime endTime = LocalDateTime.now();
        log.info("生成文档的时间: {} 秒", ChronoUnit.SECONDS.between(startTime, endTime));

        String apiDocUrl = "http://localhost:";
        Integer serverPort = env.getProperty("server.port", Integer.class);
        String contextPath = env.getProperty("server.servlet.context-path", "");
        if (serverPort != null) {
            apiDocUrl += serverPort;
            if (!contextPath.isEmpty()) {
                apiDocUrl += contextPath;
            }
            apiDocUrl += "/swagger-ui.html";
            log.info("当前文档的地址是: {}", apiDocUrl);
        }
        return docket;
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        if (enableCors) {//本机开发调用时开启,不经过网关转发请求
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedHeaders("*")
                    .allowedMethods("*");
        }
    }

}

  主要是生成swagger文档,也可以通过配置 app.swagger.enabled=false 就不会生成了,建议生成不生成。

四、优雅停服

4.1 优雅停服线程类

GracefulShutdownHook.java

package cn.alian.microservice.common.gracefullshutdown;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration;
import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry;
import org.springframework.context.ConfigurableApplicationContext;

public class GracefulShutdownHook implements Runnable {

    private static final Logger log = LoggerFactory.getLogger(GracefulShutdownHook.class);

    private final ConfigurableApplicationContext applicationContext;

    public GracefulShutdownHook(ConfigurableApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void run() {
        deregisterFromEureka();
        delayShutdownSpringContext();
        shutdownSpringContext();
    }

    private void deregisterFromEureka() {
        EurekaServiceRegistry eurekaServiceRegistry = this.applicationContext.getBean(EurekaServiceRegistry.class);
        EurekaRegistration eurekaRegistration = this.applicationContext.getBean(EurekaRegistration.class);
        log.info("Gracefully shutdown deregister from eureka");
        eurekaServiceRegistry.deregister(eurekaRegistration);
    }

    private void shutdownSpringContext() {
        log.info("Spring Application context starting to shutdown");
        this.applicationContext.close();
        log.info("Spring Application context is shutdown");
    }

    private void delayShutdownSpringContext() {
        try {
            int shutdownWaitSeconds = getShutdownWaitSeconds();
            log.info("Gonna wait for " + shutdownWaitSeconds + " seconds before shutdown SpringContext!");
            Thread.sleep((shutdownWaitSeconds * 1000));
        } catch (InterruptedException e) {
            log.error("Error while gracefulshutdown Thread.sleep", e);
        }
    }

    private int getShutdownWaitSeconds() {
        String waitSeconds = this.applicationContext.getEnvironment().getProperty("app.graceful-shutdown-wait-seconds", "120");
        return Integer.parseInt(waitSeconds);
    }

}

4.2 优雅停服调用

ConfigurableApplicationContext.java

package cn.alian.microservice.common.gracefullshutdown;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

public class GracefulSpringApplication {

    public static ConfigurableApplicationContext run(Class<?> appClazz, String... args) {
        SpringApplication app = new SpringApplication(new Class[]{appClazz});
        app.setRegisterShutdownHook(false);
        ConfigurableApplicationContext applicationContext = app.run(args);
        Runtime.getRuntime().addShutdownHook(new Thread(new GracefulShutdownHook(applicationContext)));
        return applicationContext;
    }
}

  调用此方法的run方法即可实现优雅停服

五、公共类

5.1、Mapper工具类

MapperHelper.java

package cn.alian.microservice.common.custom;

import com.github.dozermapper.core.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class MapperHelper {

    @Autowired
    private Mapper mapper;

    public <T, D> List<D> map(List<T> list, Class<D> d) {
        return list.stream().map(e -> mapper.map(e, d)).collect(Collectors.toList());
    }
}

5.2、公共返回类

ApiResponseDto.java

package cn.alian.microservice.common.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;

@Setter
@Getter
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "接口返回结果")
@ToString(exclude = "content")
public class ApiResponseDto<T> {

    /**
     * 成功
     */
    public static String CODE_SUCCESS = "0000";
    /**
     * 失败
     */
    public static String CODE_FAIL = "1000";
    /**
     * 系统异常
     */
    public static String CODE_EXCEPTION = "1001";
    /**
     * 签名错误
     */
    public static String CODE_ERR_SIGN = "1002";
    /**
     * 参数错误
     */
    public static String CODE_ERR_PARAM = "1003";
    /**
     * 业务异常
     */
    public static String CODE_BIZ_ERR = "1004";
    /**
     * 查询无数据,使用明确的参数(如id)进行查询时未找到记录时返回此错误码
     */
    public static String CODE_NO_DATA = "1005";
    /**
     * 错误的请求方法
     */
    public static String CODE_ERR_REQUEST_METHOD = "1006";
    /**
     * 未登录
     */
    public static String CODE_NOT_LOGIN = "1007";
    /**
     * 错误的请求内容类型
     */
    public static String CODE_ERR_CONTENT_TYPE = "1008";
    /**
     * 系统繁忙
     */
    public static String CODE_SYS_BUSY = "1009";
    /**
     * 系统繁忙
     */
    public static String CODE_REDIRECT = "1010";
    /**
     * 显示提示
     */
    public static String CODE_SHOW_TIP = "1011";
    /**
     * 根据bizCode进行处理
     */
    public static String CODE_DEAL_BIZ_CODE = "1012";

    public final static ApiResponseDto SUCCESS = new ApiResponseDto();

    @ApiModelProperty("状态码,0000:成功;1000:失败;1001:系统异常;1002:签名错误;" +
            "1003:参数错误;1004:业务异常;" +
            "1005:查询无数据,使用明确的参数(如id)进行查询时未找到记录时返回此错误码;" +
            "1006:错误的请求方法;1007:未登录;1008:错误的请求内容类型;1009:系统繁忙;" +
            "1010:重定向至content指定地址;1011:显示提示;1012:根据bizCode进行处理")
    private String code = CODE_SUCCESS;

    @ApiModelProperty("状态说明")
    private String msg = "success";

    @ApiModelProperty("请求是否成功")
    public boolean isSuccess() {
        return CODE_SUCCESS.equals(code);
    }

    @ApiModelProperty("结果内容")
    private T content;

    @ApiModelProperty("时间戳")
    private long timestamp = System.currentTimeMillis();

    @ApiModelProperty("业务状态码,由业务接口定义")
    private String bizCode;

    @ApiModelProperty("业务状态说明")
    private String bizMsg;

    public ApiResponseDto(T content) {
        this.content = content;
    }

    /**
     * 设置业务信息
     */
    public ApiResponseDto<T> bizInfo(String bizCode, String bizMsg) {
        this.bizCode = bizCode;
        this.bizMsg = bizMsg;
        return this;
    }

    public static <T> ApiResponseDto<T> success() {
        return new ApiResponseDto<>();
    }

    public static <T> ApiResponseDto<T> success(T content) {
        return new ApiResponseDto<T>(content);
    }

    public static <T> ApiResponseDto<T> bizResult(String bizCode, String bizMsg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setBizCode(bizCode);
        response.setBizMsg(bizMsg);
        return response;
    }

    public static <T> ApiResponseDto<T> fail(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_FAIL);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> fail(String msg, String bizCode, String bizMsg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_FAIL);
        response.setMsg(msg);
        response.setBizCode(bizCode);
        response.setBizMsg(bizMsg);
        return response;
    }

    public static <T> ApiResponseDto<T> exception(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_EXCEPTION);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> errSign(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_ERR_SIGN);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> errParam(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_ERR_PARAM);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> bizErr(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_BIZ_ERR);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> noData(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_NO_DATA);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> errRequestMethod(String msg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_ERR_REQUEST_METHOD);
        response.setMsg(msg);
        return response;
    }

    public static <T> ApiResponseDto<T> notLogin() {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_NOT_LOGIN);
        response.setMsg("未登录,请先登录");
        return response;
    }

    public static <T> ApiResponseDto<T> errContentType() {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_ERR_CONTENT_TYPE);
        response.setMsg("错误的请求内容类型");
        return response;
    }

    public static <T> ApiResponseDto<T> sysBusy() {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_SYS_BUSY);
        response.setMsg("系统繁忙");
        return response;
    }

    public static ApiResponseDto redirecet(String url) {
        ApiResponseDto response = new ApiResponseDto();
        response.setCode(CODE_REDIRECT);
        response.setMsg("重定向至content指定地址");
        response.setContent(url);
        return response;
    }

    public static <T> ApiResponseDto<T> showTip(String tip) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_SHOW_TIP);
        response.setMsg(tip);
        return response;
    }

    public static <T> ApiResponseDto<T> dealBizCode(String bizCode, String bizMsg) {
        ApiResponseDto<T> response = new ApiResponseDto<>();
        response.setCode(CODE_DEAL_BIZ_CODE);
        response.setMsg("根据bizCode进行处理");
        response.setBizCode(bizCode);
        response.setBizMsg(bizMsg);
        return response;
    }

    public static <T> ApiResponseDto<T> dealBizCode(String bizCode, String bizMsg, T content) {
        ApiResponseDto<T> response = new ApiResponseDto<>(content);
        response.setCode(CODE_DEAL_BIZ_CODE);
        response.setMsg("根据bizCode进行处理");
        response.setBizCode(bizCode);
        response.setBizMsg(bizMsg);
        return response;
    }
}

  API接口返回的封装类(本类是我们公司用得比较多的一种结构了)

5.3、公共结果校验类

ApiResponse.java

package cn.alian.microservice.common.util;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ApiResponse {

    private static final Logger log = LoggerFactory.getLogger(ApiResponse.class);

    public static Pair<Boolean, Object> check(ResponseEntity responseEntity) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        if (responseEntity == null) {
            log.warn("responseEntity is null");
            return Pair.of(false, null);
        }
        if (!responseEntity.getStatusCode().is2xxSuccessful()) {
            log.warn("status code is {}", responseEntity.getStatusCodeValue());
            return Pair.of(false, null);
        }
        Object body = responseEntity.getBody();
        if (body == null) {
            log.warn("response entity body is null");
            return Pair.of(false, null);
        }
        Method isSuccess = body.getClass().getMethod("isSuccess", new Class[0]);

        Boolean success = (Boolean) isSuccess.invoke(body, new Object[0]);

        String code = (String) PropertyUtils.getProperty(body, "code");
        String msg = (String) PropertyUtils.getProperty(body, "msg");
        if (!success) {
            log.warn("body success is false, code is {}, msg is {}", code, msg);
            return Pair.of(false, body);
        }
        return Pair.of(true, PropertyUtils.getProperty(body, "content"));
    }
    
}

  API接口结果返回的验证,一般是内部系统之间调用时用到

5.4、分页基础类

PageRequestBaseDto.java

package cn.alian.microservice.common.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ApiModel(description = "分页基础信息")
public class PageRequestBaseDto {

    @ApiModelProperty("当前页,从0开始")
    private Integer number = 0;

    @ApiModelProperty("每页记录数,默认10")
    private Integer size = 10;

}

  基础此对象就可设置对应的页码和每页记录数

5.5、分页信息类

PageDto.java

package cn.alian.microservice.common.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.Page;

import java.util.List;

@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ApiModel(description = "分页信息")
public class PageDto<T> {

    @ApiModelProperty("记录列表")
    private List<T> data;

    @ApiModelProperty("当前页,从0开始")
    private Integer number = 0;

    @ApiModelProperty("每页记录数")
    private Integer size;

    @ApiModelProperty("是否有下一页")
    public boolean isHasNext() {
        if (totalPages == null || totalPages == 0) {
            return false;
        }

        return number + 1 < totalPages;
    }

    @ApiModelProperty("是否有记录")
    public boolean hasContent() {
        return data != null && data.size() > 0;
    }

    @ApiModelProperty("总页数")
    private Integer totalPages = 0;

    @ApiModelProperty("总记录数")
    private Long totalElements = 0L;

    @ApiModelProperty("总记录数,兼容api接口")
    public Long getTotal() {
        return totalElements;
    }

    public static <T> PageDto<T> convertDtoFrom(Page<T> page) {
        return new PageDto<>(page.getContent(), page.getNumber(), page.getSize(), page.getTotalPages(), page.getTotalElements());
    }

    public static <T, D> PageDto<D> convertDtoFrom(Page<T> page, List<D> dtoList) {
        return new PageDto<>(dtoList, page.getNumber(), page.getSize(), page.getTotalPages(), page.getTotalElements());
    }

}

  分页数据返回的封装类

5.6、swagger基础信息

Swagger.java

package cn.alian.microservice.common.model;

import lombok.Data;

import java.util.List;

@Data
public class Swagger {

    private String basePath;

    private String host;

    private Info info;

    private List<Tag> tags;

    @Data
    public static class Info {
        private String groupId;
        private String artifactId;
        private String version;
        private String pack;
        private String appName;
    }

    @Data
    public static class Tag {
        private String name;
        private String description;
    }
}

  swagger生成的apiJson转换的对象

5.7、异常类

BizCodeException.java

package cn.alian.microservice.common.exception;

import lombok.Data;

@Data
public class BizCodeException extends RuntimeException {

    private String bizCode;
    private String bizMsg;
    private Object content;

    public BizCodeException(String bizCode, String bizMsg) {
        this.bizCode = bizCode;
        this.bizMsg = bizMsg;
    }

    public BizCodeException(String bizCode, String bizMsg, Object content) {
        this.bizCode = bizCode;
        this.bizMsg = bizMsg;
        this.content = content;
    }

}

BizLoginException.java

package cn.alian.microservice.common.exception;

/**
 * 业务处理中验证当前用户需要重新登录
 */
public class BizLoginException extends RuntimeException {

    public BizLoginException(String message) {
        super(message);
    }

}

BizTipException.java

package cn.alian.microservice.common.exception;

import lombok.Data;

/**
 * 业务处理中需要向前端展示提示信息抛出此异常
 * 业务代码中可不输出提示信息,提示信息会统一输出
 */
@Data
public class BizTipException extends RuntimeException {

    private String bizCode;
    private String bizMsg;

    public BizTipException(String message) {
        super(message);
    }

    public BizTipException(String message, String bizCode, String bizMsg) {
        super(message);
        this.bizCode = bizCode;
        this.bizMsg = bizMsg;
    }

}

几个异常类没啥好说的

  • 业务码异常
  • 未登录异常
  • 提示异常

5.8、全局异常处理类

GlobalExceptionHandler.java

package cn.alian.microservice.common.custom;

import cn.alian.microservice.common.dto.ApiResponseDto;
import cn.alian.microservice.common.exception.BizCodeException;
import cn.alian.microservice.common.exception.BizLoginException;
import cn.alian.microservice.common.exception.BizTipException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.internal.engine.ConstraintViolationImpl;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

@Slf4j
@Component
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(MethodArgumentNotValidException exception) {
        String msg = exception.getBindingResult().getAllErrors().stream()
                .map(e -> {
                    boolean contains = e.contains(ConstraintViolationImpl.class);
                    if (!contains) {
                        return e.getDefaultMessage();
                    }
                    ConstraintViolationImpl unwrap = e.unwrap(ConstraintViolationImpl.class);
                    String messageTemplate = unwrap.getMessageTemplate();
                    String message = unwrap.getMessage();
//message is 不能为null,messageTemplate is {javax.validation.constraints.NotNull.message}
//message is 应用id不能为空,messageTemplate is 应用id不能为空
//                        log.info("message is {},messageTemplate is {}",message,messageTemplate);
                    if (message.equals(messageTemplate)) {
                        return message;
                    }

                    if (e instanceof FieldError) {
                        FieldError err = (FieldError) e;
                        String defaultMessage = err.getDefaultMessage();
                        String field = err.getField();
                        return field + defaultMessage;
                    }

                    return e.getDefaultMessage();

                })
//                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(";"));
        return logWarn(exception.getMessage(), null, ApiResponseDto.errParam(msg));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(ConstraintViolationException exception) {
        return logWarn(null, exception, ApiResponseDto.errParam("参数错误"));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(MissingServletRequestParameterException exception) {
        return logWarn(exception.getMessage(), null, ApiResponseDto.errParam("参数错误"));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(HttpMessageNotReadableException exception) {
        return logWarn(null, exception, ApiResponseDto.errParam("参数错误"));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto<?> handle(HttpRequestMethodNotSupportedException exception, HttpServletRequest request) {
        return logWarn(request.getRequestURI() + " " + exception.getMessage(), null, ApiResponseDto.errRequestMethod("请求方法错误"));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(BizTipException exception) {
//        log.warn(exception.getClass().getCanonicalName()+": "+exception.getMessage());
        return logWarn(exception.getMessage(), null,
                ApiResponseDto.showTip(exception.getMessage()).bizInfo(exception.getBizCode(), exception.getBizMsg()));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(BizCodeException exception) {
        return logWarn(exception.getMessage(), null,
                ApiResponseDto.dealBizCode(exception.getBizCode(), exception.getBizMsg(), exception.getContent()));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(BizLoginException exception) {
        ApiResponseDto<Object> dto = ApiResponseDto.notLogin();
        return logWarn(exception.getMessage(), null, dto);
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(MaxUploadSizeExceededException exception) {
        return logError(null, exception, ApiResponseDto.bizErr("文件大小超过限制"));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(Exception exception) {
        if ("org.springframework.boot.autoconfigure.klock.handler.KlockTimeoutException"
                .equals(exception.getClass().getCanonicalName())) {
            return logWarn(exception.getMessage(), null, ApiResponseDto.sysBusy());
        }
        return logError(null, exception, ApiResponseDto.exception("系统异常"));
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(HttpMediaTypeNotSupportedException exception, HttpServletRequest request) {
        return logError(request.getRequestURI() + " " + exception.getMessage(), exception, ApiResponseDto.errContentType());
    }

    @ExceptionHandler
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ApiResponseDto handle(ServletRequestBindingException exception) {
        String message = exception.getMessage();
        if (StringUtils.startsWith(message, "Missing session attribute")) {
            ApiResponseDto<Object> dto = ApiResponseDto.notLogin();
            return logError(message, null, dto);
        }
        return logError(null, exception, ApiResponseDto.exception("系统异常"));
    }

    private static ApiResponseDto logWarn(String msg, Exception e, ApiResponseDto responseDto) {
        long timestamp = responseDto.getTimestamp();
        String m = "timestamp is " + timestamp;
        if (msg != null) {
            m += ", " + msg;
        }
        if (e == null) {
            log.warn(m);
        } else {
            log.warn(m, e);
        }
        return responseDto;
    }

    private static ApiResponseDto logError(String msg, Exception e, ApiResponseDto responseDto) {
        long timestamp = responseDto.getTimestamp();
        String m = "timestamp is " + timestamp;
        if (msg != null) {
            m += ", " + msg;
        }
        log.error(m, e);
        return responseDto;
    }

}

  全局异常处理不要突然弄出个500啥的返回

六、spring.factories

resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.alian.microservice.common.config.Config

七、打包发布脚本

deploy.bat

cd %~dp0
cd..

call mvn clean source:jar deploy -Dmaven.test.skip=true

cd bin
pause

install.bat

cd %~dp0
cd..

call mvn clean install -Dmaven.test.skip=true

cd bin
pause

  如果是发布到私服就用第一个,如果是本地开发可以用第二个安装到本地。

八、使用

  引入以下依赖即可享受以上所有的功能了

 <dependency>
     <groupId>cn.alian.microservice</groupId>
     <artifactId>common-api</artifactId>
     <version>1.0.0-SNAPSHOT</version>
 </dependency>

结语

  本文就是一个封装,各种封装,看起来啰嗦,但是在实际的工作中很重要,尤其是规范,这也是很多人或者团队做不到的一点。比如你API统一返回都定义好了,对前端来说,统一的请求和解析也就差不多统一了。文档都生成了,前端不用问你要具体接口名称和字段了,直接打开自动生成的swagger文档即可。还有其他的分页及异常什么的都是一样,说白了就是磨刀不误砍柴工。

 类似资料: