在现实的开发中,往往是多模块开发。把一些共通的代码或配置提取出来,方便业务模块引用。以达到代码复用和快速开发。
在本篇中,我们引入nacos,swagger,在业务服务器中通过接口访问可以读取nacos中配置的值
负责管理maven引用的版本,设置一些全局变量
负责放一些与servlet服务器无关的maven引用,以及一些与servlet-web无关的共通代码
由于gateway与servlet服务器不兼容,所以要把共通代码分成2份
和web功能相关的maven引用、配置、拦截器等。该模块是个聚合模块,其下有4个子模块
引入swagger,全局controller返回值封装,全局异常捕捉等
引入mysql,mybatis-plus,以及mybatis-plus相关的配置
引入redis
把web,mysql,redis引入,方便业务模块一键引入。
同时也方便一些业务,可能不需要mysql或者redis,排除掉相关的模块
具体的业务模块
1)点击file->new->project
把groupId 填为 indi.zhifa.recipe
name和artifactId填为bailan
2)删除src
3)在bailan文件夹创建.gitignore
target/
/*/target
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
# External tool builders
.externalToolBuilders/
<groupId>indi.zhifa.recipegroupId>
<artifactId>bailanartifactId>
<version>1.0-SNAPSHOTversion>
group第一个单词使用com(公司)或indi(个人)
第二个单词使用 公司名(或笔名)
第三个单词使用大项目名
artifactId 使用小项目名
在老家做开发,通常使用伪单体模式。引用spring-cloud和阿里巴巴的nacos作为基础组建,方便可能的服务间调用。有的公司使用配置中心,有的公司不使用。
官网上有SpringBoot,SpringCloud和alibaba版本的关系,我们通常选择第二新的版本。
由于我个人比较喜欢长连接的nacos(v>2.0),但Spring2.4相比2.3改动很大,为保证拓展性,决定才用2.4以后版本。
所以我更倾向2021.1的阿里巴巴版本
那么,spring cloud使用2020.0.5,SpringBoot使用2.4.13
故parent如下配置
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.13version>
<relativePath/>
parent>
properties一般用来定义各依赖的版本,目前我是这样写的,大家其实可以直接抄
<properties>
<spring-boot.version>2.4.13spring-boot.version>
<spring-cloud.version>2020.0.5spring-cloud.version>
<cloud-alibaba.version>2021.1cloud-alibaba.version>
<nacos-client.version>2.0.20.graalnacos-client.version>
<mybatis-plus.version>3.5.1mybatis-plus.version>
<commons.lang3.version>3.9commons.lang3.version>
<commons-pool2.version>2.11.1commons-pool2.version>
<logback.version>1.2.3logback.version>
<dozen.version>5.5.1dozen.version>
<knife4j.version>3.0.3knife4j.version>
<fastjson.version>2.0.23fastjson.version>
<hutool.version>5.8.5hutool.version>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
properties>
dependencyManagement通常做版本的管理,在根pom配置后,子模块可以不写version,在根pom统一管理。
dependencyManagement中写的内容包括三部分
#1 spring-cloud和阿里巴巴的dependencies的import
#2 子模块的信息,在这里写了,子模块引用就不需要写version了
#3 一般的引用
dependencyManagement 可以单独写一个xml文件,方便子类聚合模块引用
在根目录的pom,只需要这样写:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>bailan-dependencyartifactId>
<version>${project.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
<version>${nacos-client.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-extensionartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>p6spygroupId>
<artifactId>p6spyartifactId>
<version>${p6spy.version}version>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
<version>${freemarker.version}version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
<version>${knife4j.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>${springfox.version}version>
dependency>
<dependency>
<groupId>net.sf.dozergroupId>
<artifactId>dozerartifactId>
<version>${dozen.version}version>
dependency>
<dependency>
<groupId>net.sf.dozergroupId>
<artifactId>dozer-springartifactId>
<version>${dozen.version}version>
dependency>
<dependency>
<groupId>io.craftsmangroupId>
<artifactId>dozer-jdk8-supportartifactId>
<version>1.0.6version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>${commons.lang3.version}version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool.version}version>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2artifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2-extension-spring5artifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-annotationartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>jakarta.xml.bindgroupId>
<artifactId>jakarta.xml.bind-apiartifactId>
<version>${xml-bind.version}version>
dependency>
<dependency>
<groupId>com.sun.xml.bindgroupId>
<artifactId>jaxb-implartifactId>
<version>${xml-bind.version}version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>${jwt.version}version>
dependency>
dependencies>
dependencyManagement>
denpendency.xml
放入各模块的版本,方便在子孙模块中使用
<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.0modelVersion>
<groupId>indi.zhifa.recipegroupId>
<artifactId>bailan-dependencyartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<properties>
<spring-boot.version>2.4.13spring-boot.version>
<spring-cloud.version>2020.0.5spring-cloud.version>
<cloud-alibaba.version>2021.1cloud-alibaba.version>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-commonartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-web-commonartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-mysqlartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-redisartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-web-allartifactId>
<version>${project.version}version>
dependency>
...
dependencies>
dependencyManagement>
project>
profiles 一般用于多环境配置
<profiles>
<profile>
<id>localid>
<properties>
<package.environment>localpackage.environment>
properties>
profile>
<profile>
<id>devid>
<properties>
<package.environment>devpackage.environment>
properties>
<activation>
<activeByDefault>trueactiveByDefault>
activation>
profile>
<profile>
<id>prodid>
<properties>
<package.environment>prodpackage.environment>
properties>
profile>
profiles>
本期中,common主要包括以下几个方面
#1 基础类库
#2 spring-cloud相关的依赖
#3 yml配置库
#4 通用的返回值封装
#5 通用的异常
#6 通用util
<parent>
<artifactId>bailanartifactId>
<groupId>indi.zhifa.recipegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>framework-commonartifactId>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2artifactId>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2-extension-spring5artifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>net.sf.dozergroupId>
<artifactId>dozerartifactId>
dependency>
<dependency>
<groupId>net.sf.dozergroupId>
<artifactId>dozer-springartifactId>
dependency>
<dependency>
<groupId>io.craftsmangroupId>
<artifactId>dozer-jdk8-supportartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-annotationartifactId>
<version>${mybatis-plus.version}version>
dependency>
dependencies>
web-common模块是个聚合模块,不引用任何库,只管理子模块
<parent>
<artifactId>bailanartifactId>
<groupId>indi.zhifa.recipegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>common-webartifactId>
<packaging>pompackaging>
<modules>
<module>framework-web-commonmodule>
<module>framework-mysqlmodule>
<module>framework-redismodule>
<module>framework-web-allmodule>
modules>
<artifactId>framework-web-commonartifactId>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-commonartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jsonartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
<version>${nacos-client.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
dependencies>
<artifactId>framework-mysqlartifactId>
<dependencies>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-commonartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-extensionartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
dependencies>
<artifactId>framework-redisartifactId>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-commonartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
dependencies>
<artifactId>framework-web-allartifactId>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-web-commonartifactId>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-redisartifactId>
dependency>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-mysqlartifactId>
dependency>
dependencies>
<artifactId>busyartifactId>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-web-allartifactId>
<exclusions>
<exclusion>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-mysqlartifactId>
exclusion>
<exclusion>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-redisartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
dependencies>
yml配置读取,通用返回值,通用异常,fastjson,通用工具类
目录结构如图:
我通常喜欢使用fastjson作为SpringBoot的序列化工具,所以需要加一个配置
其中要注意的是,Long类型通常用作id,会超出前端JavaScript的存储范围,所以要以字符串方式显示
国内的时间,比较喜欢使用老版的"yyyy-MM-dd HH:mm:ss"
目前代码如下:
@Configuration
public class FastJsonHttpMessageConverterConfig {
@Bean
public FastJsonConfig getDefaultFastJsonConfig(){
FastJsonConfig config = new FastJsonConfig();
config.setWriterFeatures(
// 保留 Map 空的字段
JSONWriter.Feature.WriteMapNullValue,
// 将 String 类型的 null 转成""
JSONWriter.Feature.WriteNullStringAsEmpty,
// 将 Number 类型的 null 转成 0
//SerializerFeature.WriteNullNumberAsZero,
// 将 List 类型的 null 转成 []
JSONWriter.Feature.WriteNullListAsEmpty,
// 将 Boolean 类型的 null 转成 false
JSONWriter.Feature.WriteNullBooleanAsFalse,
// 漂亮的输出,这里是学习用,正式工程请注释掉
JSONWriter.Feature.PrettyFormat,
// 把Long转化为String
JSONWriter.Feature.WriteLongAsString,
// 把BigDecimal转化为String
JSONWriter.Feature.BrowserCompatible,
// 枚举
JSONWriter.Feature.WriteEnumsUsingName);
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setWriterFilters();
return config;
}
@Bean
public FastJsonHttpMessageConverter getFastJsonHttpMessageConverter(FastJsonConfig pFastJsonConfig){
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
converter.setDefaultCharset(StandardCharsets.UTF_8);
converter.setFastJsonConfig(pFastJsonConfig);
List<MediaType> mediaTypeList = new ArrayList<>();
// 解决中文乱码问题,相当于在 Controller 上的 @RequestMapping 中加了个属性 produces = "application/json"
mediaTypeList.add(MediaType.APPLICATION_JSON);
mediaTypeList.add(MediaType.TEXT_HTML);
converter.setSupportedMediaTypes(mediaTypeList);
return converter;
}
}
如果想在代码中使用独立的yml配置(非application.yml),需要额外增加配置类
@Configuration
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Properties propertiesFromYaml = loadYamlIntoProperties(resource);
String sourceName = name != null ? name : resource.getResource().getFilename();
return new PropertiesPropertySource(sourceName, propertiesFromYaml);
}
private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException {
try {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
return factory.getObject();
} catch (IllegalStateException e) {
// for ignoreResourceNotFound
Throwable cause = e.getCause();
if (cause instanceof FileNotFoundException) {
throw (FileNotFoundException) e.getCause();
}
throw e;
}
}
}
通用返回的类
@Data
@Slf4j
@Schema(name = "通用返回")
public class RestResponse<T>{
@Schema(name = "返回数据")
private T data;
@Schema(name = "返回码,200为正常,非200即为错误")
private int code;
@Schema(name = "状态信息")
private String message;
public RestResponse() {
code = HttpStatus.OK.value();
}
public RestResponse(T pData) {
code = HttpStatus.OK.value();
data = pData;
}
public RestResponse(int pCode, String errMsg) {
code = pCode;
message = errMsg;
}
public RestResponse(int pCode, T pData, String errMsg) {
code = pCode;
data = pData;
message = errMsg;
}
public static <T> RestResponse<T> builderJsonResult(int code,T data,String msg){
return new RestResponse<T>(code,data,msg);
}
public static <T> RestResponse<T> ok(T pData){
return new RestResponse<T>(pData);
}
public static RestResponse error(String pMsgErr){
return new RestResponse(500,pMsgErr);
}
public static RestResponse error(int pCode, String pMsgErr){
return new RestResponse(pCode,pMsgErr);
}
}
通用异常类
public class ServiceException extends RuntimeException {
@Getter
private final int code;
public String getMsg(){
return getMessage();
}
public ServiceException(String msg) {
super(msg);
//this.msg = msg;
this.code = 500;
}
public ServiceException(String msg, Throwable e) {
super(msg, e);
this.code = 500;
}
public ServiceException(int pErrorCode, String msg) {
super(msg);
this.code = pErrorCode;
}
public ServiceException(int pErrorCode, String msg, Throwable e) {
super(msg, e);
this.code = pErrorCode;
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
public @interface ZfRestController {
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OriginalControllerReturnValue {
}
util包代码暂时不展示了,后面用到再说
由于本篇不打算涉及redis和mysql的操作,故只展示web部分的代码
个人比较喜欢小刀版本的swagger,这个版本的swagger会记录接口的值(即使关闭浏览器)。
这样的话,在测试时会十分方便
SwaggerProperties:
@Configuration
@Data
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
/**
* 是否启用swagger
*/
private boolean enable = true;
/**
* swagger 组名
*/
private String groupName = "业务接口";
/**
* 扫描包配置
*/
private String apiPackage = "indi.zhifa.**.controller";
/**
* 路径正则
*/
private String apiRegex = "/api/**";
/**
* 系统标题
*/
private String title = "业务接口";
/**
* 系统描述
*/
private String description = "业务接口文档";
/**
* 系统版本
*/
private String version = "1.0.0";
/**
* 联系人姓名
*/
private String name = "芝法酱";
/**
* 联系人邮箱
*/
private String email = "[email protected]";
/**
* 联系人地址
*/
private String url = "https://github.com/hataksumo";
}
Knife4jConfiguration
@Configuration
@EnableOpenApi
public class Knife4jConfiguration {
public static final String KEY_AUTHORIZATION = "Authorization";
private final SwaggerProperties mProperties;
public Knife4jConfiguration(SwaggerProperties pProperties){
mProperties = pProperties;
}
@Bean
public Docket knife4jDocket() {
Docket docket=new Docket(DocumentationType.OAS_30)
.groupName(mProperties.getGroupName())
.pathMapping("/")
.enable(mProperties.isEnable())
.apiInfo(getApiInfo())
.select()
.apis(basePackage(mProperties.getApiPackage()))
.paths(PathSelectors.ant(mProperties.getApiRegex()))
.build();
return docket;
}
private ApiInfo getApiInfo(){
return new ApiInfoBuilder()
.title(mProperties.getTitle())
.description(mProperties.getDescription())
.contact(new Contact(mProperties.getName(),mProperties.getUrl(),mProperties.getEmail()))
.version(mProperties.getVersion())
.build();
}
public static Predicate<RequestHandler> basePackage(final String basePackage) {
return input -> declaringClass(input).map(handlerPackage(basePackage)).orElse(true);
}
private static Function<Class<?>, Boolean> handlerPackage(final String basePackage) {
return input -> {
// 循环判断匹配
for (String strPackage : basePackage.split(";")) {
boolean isMatch = input.getPackage().getName().startsWith(strPackage);
if (isMatch) {
return true;
}
}
return false;
};
}
private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
return Optional.ofNullable(input.declaringClass());
}
}
CustomWebMvcConfigurer
@Configuration
@AllArgsConstructor
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
private final FastJsonHttpMessageConverter fastJsonHttpMessageConverter;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Iterator<HttpMessageConverter<?>> it = converters.iterator();
while (it.hasNext()) {
HttpMessageConverter<?> converter = it.next();
if (converter instanceof StringHttpMessageConverter) {
StringHttpMessageConverter stringHttpMessageConverter = (StringHttpMessageConverter) converter;
if(!stringHttpMessageConverter.getDefaultCharset().equals(StandardCharsets.UTF_8)){
it.remove();
}
}
if (converter instanceof MappingJackson2HttpMessageConverter ||
converter instanceof FastJsonHttpMessageConverter) {
it.remove();
}
}
converters.add(fastJsonHttpMessageConverter);
}
}
LocalDateTimeSerializerConfig
@Slf4j
@Configuration
public class LocalDateTimeSerializerConfig {
DateTimeFormatter df;
@PostConstruct
protected void init(){
df = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral(' ')
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.toFormatter();
}
@Bean
public Converter<String, LocalDateTime> localDateTimeConvert() {
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(@NotNull String source) {
LocalDateTime time = LocalDateTimeUtil.parse(source);
return time;
}
};
}
}
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(JsonParseException.class)
@ResponseBody
public RestResponse jsonValidException() {
return RestResponse.error("Json格式错误!");
}
/**
* Description: 业务主动抛出的异常
* @author: bixuejun([email protected])
* @date: 2021/11/27 19:29
* @param
* @return
*/
@ExceptionHandler(ServiceException.class)
@ResponseBody
public RestResponse otherException(ServiceException ex) {
RestResponse result = null;
if (null != ex){
log.debug("op=global_exception_handler_log_ServiceException", ex);
int code = ex.getCode();
if(code ==0 || code == 500){
result = RestResponse.error(ex.getMsg());
}else{
result = RestResponse.error(code,ex.getMsg());
}
}
return result;
}
/**
* Description: 系统抛出的未知异常
* @author: bixuejun([email protected])
* @date: 2021/11/27 19:26
* @param
* @return
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public RestResponse handle(Exception e) {
log.error("op=global_exception_handler_log_unknowError", e);
return RestResponse.error("发生未知错误 "+e.toString());
}
}
@RestControllerAdvice(annotations ={ZfRestController.class})
public class GlobalRestfulResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
Annotation originalControllerReturnValue = returnType.getMethodAnnotation(OriginalControllerReturnValue.class);
if (originalControllerReturnValue != null) {
return false;
}
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if ( body instanceof RestResponse){
return body;
}
if( body instanceof String){
return body;
}
return RestResponse.ok(body);
}
}
public class DtoEntityUtil {
static DozerBeanMapper mapper;
public static void init(){
mapper = new DozerBeanMapper();
mapper.setMappingFiles(Collections.singletonList("dozerJdk8Converters.xml"));
}
public static <D, E> E trans(D t, Class<E> clazz) {
if (t == null) {
return null;
}
return mapper.map(t, clazz);
}
public static <D, E> List<E> trans(D[] ts, Class<E> clazz) {
List<E> es = new ArrayList<E>();
if (ts == null) {
return es;
}
for (D d : ts) {
E e = (E) trans(d, clazz);
if (e != null) {
es.add(e);
}
}
return es;
}
public static <D, E> List<E> trans(List<D> ts, Class<E> clazz) {
List<E> es = new ArrayList<E>();
if (ts == null) {
return es;
}
for (D d : ts) {
E e = (E) trans(d, clazz);
if (e != null) {
es.add(e);
}
}
return es;
}
public static <T> void copy(T dst, T src){
mapper.map(src, dst);
}
}
本次工程,实现启动时读取app节点下的角色,并支持内存级别的角色增删改查。
暴露接口查看color和energy,并支持nacos动态修改
我通常把程序自定义的配置放在app节点下,这样方便管理
使用ConfigurationProperties注解,如图所示
@Data
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperty {
String color;
int energy;
List<PlayerProperty> players;
}
@Data
public class PlayerProperty {
String name;
String weapon;
String desc;
}
app:
color: orange
energy: 7
players:
- name: "宵宫"
weapon: "BOW"
- name: "影"
weapon: "POLEARM"
- name: "心海"
weapon: "CATALYST"
- name: "优菈"
weapon: "CLAYMORE"
- name: "温迪"
weapon: "BOW"
- name: "班尼特"
weapon: "SWORD"
- name: "钟离"
weapon: "POLEARM"
- name: "北斗"
weapon: "CLAYMORE"
- name: "行秋"
weapon: "SWORD"
swagger:
enable: true
group-name: "业务接口"
api-package: indi.zhifa.recipe.bailan.busy.controller.api
api-regex: "/api/**"
title: "业务接口"
description: "业务接口,提供nacos属性的访问和模拟用户的增删改查"
version: "1.0.0"
name: "芝法酱"
email: "[email protected]"
url: "https://github.com/hataksumo"
@Data
public class PlayerDto {
String name;
String desc;
EWeapon weapon;
}
@AllArgsConstructor
public enum EWeapon {
DEFFAULT(0,"deffault","默认"),
SWORD(1,"剑","短手剑"),
POLEARM(2,"长柄","长柄武器"),
CLAYMORE(3,"大剑","双手剑"),
BOW(4,"弓","弓"),
CATALYST(5,"书","法器");
@EnumValue
@Getter
int code;
@Getter
String name;
@Getter
String desc;
}
@Data
public class PlayerEntity {
Long id;
String name;
String desc;
EWeapon weapon;
LocalDateTime createTime;
LocalDateTime modifyTime;
}
通常,我们写service时,会先定义好接口,这样可以帮助我们想清楚要写些什么。
而且可以使代码的文档更加清晰,在别人调用时,不需要看令人头疼的实现,只看接口上的文档即可。
public interface IPlayerService {
/**
* 读取yml中初始配置的角色
* @param pIniPlayers 角色数组
*/
void init(List<PlayerProperty> pIniPlayers);
/**
* 新增角色
*
* @param pPlayerDto 角色配置数据
* @return PlayerEntity 角色实体
*/
PlayerEntity create(PlayerDto pPlayerDto);
/**
* 修改角色
*
* @param pId 角色Id
* @param pPlayerDto 角色信息
* @return PlayerEntity 角色实体
*/
PlayerEntity edit(Long pId, PlayerDto pPlayerDto);
/**
* 删除角色
*
* @param pId 角色Id
* @return 是否删除成功
*/
boolean delete(Long pId);
/**
* 查找角色信息
*
* @param pId 角色Id
* @return 角色信息
*/
PlayerEntity info(Long pId);
/**
* 查询角色列表
*
* @param pName 角色名
* @param pWeapon 武器
* @return List 角色列表
*/
List<PlayerEntity> list(String pName, EWeapon pWeapon);
}
在impl包中写上实现,加上@Component接口后,spring就可以找到响应接口的实现了
由于本篇中没有引用数据库,这里我仅仅做简单的内存实现
@Component
public class PlayerServiceImpl implements IPlayerService {
Long mId;
Map<Long,PlayerEntity> mPlayerEntityIdMap;
Map<String,PlayerEntity> mPlayerEntityNameMap;
public PlayerServiceImpl(){
mId = 0L;
mPlayerEntityIdMap = new HashMap<>();
mPlayerEntityNameMap = new HashMap<>();
}
@Override
public void init(List<PlayerProperty> pIniPlayers) {
for(PlayerProperty playerProperty : pIniPlayers){
PlayerEntity playerEntity = new PlayerEntity();
playerEntity.setId(getId());
playerEntity.setCreateTime(LocalDateTime.now());
playerEntity.setName(playerProperty.getName());
playerEntity.setWeapon(EWeapon.valueOf(playerProperty.getWeapon()));
playerEntity.setDesc(playerProperty.getDesc());
addPlayer(playerEntity);
}
}
@Override
public PlayerEntity create(PlayerDto pPlayerDto) {
if(mPlayerEntityNameMap.containsKey(pPlayerDto.getName())){
throw new ServiceException("已经存在名为"+pPlayerDto.getName()+"的角色");
}
PlayerEntity playerEntity = DtoEntityUtil.trans(pPlayerDto,PlayerEntity.class);
playerEntity.setId(getId());
playerEntity.setCreateTime(LocalDateTime.now());
addPlayer(playerEntity);
return playerEntity;
}
@Override
public PlayerEntity edit(Long pId, PlayerDto pPlayerDto) {
PlayerEntity playerEntity = checkPlayer(pId);
DtoEntityUtil.copy(playerEntity,pPlayerDto);
return playerEntity;
}
@Override
public boolean delete(Long pId) {
PlayerEntity playerEntity = checkPlayer(pId);
mPlayerEntityIdMap.remove(playerEntity.getId());
mPlayerEntityNameMap.remove(playerEntity.getName());
return true;
}
@Override
public PlayerEntity info(Long pId) {
PlayerEntity playerEntity = checkPlayer(pId);
return playerEntity;
}
@Override
public List<PlayerEntity> list(String pName, EWeapon pWeapon) {
if(StringUtils.hasText(pName)){
PlayerEntity playerEntity = checkPlayer(pName);
return Arrays.asList(playerEntity);
}
List<PlayerEntity> playerEntityList = new ArrayList<>(mPlayerEntityIdMap.size());
for(PlayerEntity playerEntity : mPlayerEntityIdMap.values()){
if(null != pWeapon){
if(playerEntity.getWeapon() == pWeapon){
playerEntityList.add(playerEntity);
}
}else{
playerEntityList.add(playerEntity);
}
}
return playerEntityList;
}
protected Long getId(){
synchronized (this.mId){
++mId;
return mId;
}
}
protected void addPlayer(PlayerEntity pPlayer){
mPlayerEntityIdMap.put(pPlayer.getId(),pPlayer);
mPlayerEntityNameMap.put(pPlayer.getName(),pPlayer);
}
protected PlayerEntity checkPlayer(Long pId){
PlayerEntity playerEntity = mPlayerEntityIdMap.get(pId);
if(null == playerEntity){
throw new ServiceException("没有找到Id为"+pId+"的角色");
}
return playerEntity;
}
protected PlayerEntity checkPlayer(String pName){
PlayerEntity playerEntity = mPlayerEntityNameMap.get(pName);
if(null == playerEntity){
throw new ServiceException("没有找到Name为"+pName+"的角色");
}
return playerEntity;
}
}
通常,我们希望在服务器启动时执行一段代码。可以在component中使用@PostConstruct,也可以使用一个统一的类,继承自CommandLineRunner来实现。我个人倾向后者,因为正式的项目可能要在启动时做很多事情,放一起方便管理。
@Component
@RequiredArgsConstructor
public class AppInit implements CommandLineRunner {
private final IPlayerService mPlayerService;
private final AppProperty mAppProperty;
@Override
public void run(String... args) throws Exception {
DtoEntityUtil.init();
mPlayerService.init(mAppProperty.getPlayers());
}
}
我们写一个接口来测试下nacos的属性自动刷新功能。
大家可以尝试下nacos中配置改变前后,时候接口返回值会跟着变化
@Api(tags = "1.Nacos接口")
@RequestMapping("/api/nacos")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class NacosPropertyApi {
private final AppProperty mAppProperty;
@Operation(summary = "获取颜色")
@GetMapping("/color")
public RestResponse<String> getColor(){
return RestResponse.ok(mAppProperty.getColor());
}
@GetMapping("/energy")
public int getEnergy(){
return mAppProperty.getEnergy();
}
}
一般说来,对于一个实体,我们都会写如下接口:
1、新增
PostMapping(“”)
实体VO create(@RequestBody 实体DTO)
2、修改
@PutMapping(“/{id}”)
实体VO create(@PathVariable(“id”) Long pId, @RequestBody 实体DTO)
3、删除
@DeleteMapping(“/{id}”)
RestResponse delete(@PathVariable(“id”) Long pId)
4、获取信息
@GetMapping(“/{id}”)
实体VO info(@PathVariable(“id”) Long pId)
5、分页接口
@GetMapping(“/page”)
Page<实体VO> page(@RequestParam(“current”) int pCurrent,
@RequestParam(“size”) int pSize,
…各查询条件)
由于本例没接入数据库,就是用list替代
@Slf4j
@Api(tags = "2.用户接口")
@RequestMapping("/api/user")
@RequiredArgsConstructor
@ZfRestController
public class PlayerApi {
private final IPlayerService mPlayerService;
@Operation(summary = "创建用户")
@PostMapping("")
PlayerEntity create(@RequestBody PlayerDto pPlayerDto){
PlayerEntity playerEntity = mPlayerService.create(pPlayerDto);
return playerEntity;
}
@Operation(summary = "修改用户")
@PutMapping("/{id}")
PlayerEntity edit(@PathVariable("id") @Parameter(description = "用户Id") Long pId,
@RequestBody PlayerDto pPlayerDto){
PlayerEntity playerEntity = mPlayerService.edit(pId,pPlayerDto);
log.info("{}修改为{}",pId, JSON.toJSONString(pPlayerDto));
return playerEntity;
}
@Operation(summary = "删除用户")
@DeleteMapping("/{id}")
RestResponse<String> delete(@PathVariable("id") @Parameter(description = "用户Id") Long pId){
if(mPlayerService.delete(pId)){
return RestResponse.ok("删除成功");
}
return RestResponse.ok("删除失败");
}
@Operation(summary = "获取用户信息")
@GetMapping("/{id}")
PlayerEntity info(@PathVariable("id") @Parameter(description = "用户Id") Long pId){
PlayerEntity playerEntity = mPlayerService.info(pId);
return playerEntity;
}
@Operation(summary = "获取用户列表")
@GetMapping("/list")
List<PlayerEntity> list(@RequestParam(name = "name",required = false) @Parameter(description = "用户名") String pName,
@RequestParam(name = "weapon",required = false) @Parameter(description = "武器") EWeapon pWeapon){
List<PlayerEntity> playerEntityList = mPlayerService.list(pName,pWeapon);
return playerEntityList;
}
}
在实际开发测试中,我们通常是用swagger做测试工具。
相比postman,不需要再手动编辑一遍接口,大大节省时间,提升开发效率
本例是用了加强版的小刀swagger,访问地址是/doc.html