├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── HELP.md ├── README.md ├── auth ├── pom.xml └── src │ ├── main │ └── java │ │ └── top │ │ └── rizon │ │ └── springbestpractice │ │ └── auth │ │ ├── AuthObjMapper.java │ │ ├── AuthService.java │ │ ├── AuthenticationInterceptor.java │ │ └── SimpleAuthUser.java │ └── test │ └── java │ └── top │ └── rizon │ └── springbestpractice │ └── AppTest.java ├── common ├── pom.xml └── src │ ├── main │ └── java │ │ └── top │ │ └── rizon │ │ └── springbestpractice │ │ └── common │ │ ├── aspect │ │ ├── AutoRepairPageAspect.java │ │ └── MethodLogAspect.java │ │ ├── cache │ │ ├── CacheHandlerInterceptor.java │ │ └── RequestScopedCacheManager.java │ │ ├── constant │ │ └── DateFormatType.java │ │ ├── exception │ │ ├── AssertFailExceptionBase.java │ │ ├── AuthFailedException.java │ │ └── BaseServerException.java │ │ ├── handler │ │ ├── AbstractAuthHandler.java │ │ ├── ExceptionHandlers.java │ │ ├── MessageSourceHandler.java │ │ └── RestClientErrorExceptionHandler.java │ │ ├── model │ │ ├── dto │ │ │ └── AuthUser.java │ │ ├── request │ │ │ ├── BaseReqParam.java │ │ │ └── PageParam.java │ │ └── response │ │ │ ├── PageResponse.java │ │ │ └── Response.java │ │ └── utils │ │ ├── Assert.java │ │ ├── AuthUtil.java │ │ ├── DataDate.java │ │ ├── DateTimeUtils.java │ │ ├── ExceptionUtils.java │ │ ├── JacksonUtil.java │ │ ├── MapUtils.java │ │ ├── ObjUtil.java │ │ ├── ResponseUtil.java │ │ ├── StreamUtil.java │ │ ├── TraceUtils.java │ │ └── http │ │ ├── BaseAuthHeaderHttpRequestInterceptor.java │ │ ├── RestTemplateAuthConfig.java │ │ └── SimpleRestTemplateUtils.java │ └── test │ └── java │ └── top │ └── rizon │ └── springbestpractice │ ├── AppTest.java │ └── common │ └── utils │ ├── MapUtilsTest.java │ └── StreamUtilTest.java ├── dao ├── pom.xml └── src │ └── main │ └── java │ └── top │ └── rizon │ └── springbestpractice │ └── dao │ ├── config │ └── MybatisPlusConfig.java │ ├── helper │ ├── UserHelper.java │ └── impl │ │ └── UserHelperImpl.java │ ├── mapper │ ├── HistoryMapper.java │ └── UserMapper.java │ ├── po │ ├── HistoryPo.java │ └── User.java │ └── utils │ └── dynamictblname │ ├── DynamicTableNameParser.java │ ├── DynamicTableNameUtils.java │ ├── ITableNameHandler.java │ └── TableNameParser.java ├── mvnw ├── mvnw.cmd ├── pom.xml └── web ├── deploy ├── Dockerfile ├── bin │ └── app.sh ├── conf │ └── application-test.yml └── package_jar.sh ├── pom.xml └── src ├── main ├── java │ └── top │ │ └── rizon │ │ └── springbestpractice │ │ └── web │ │ ├── DemoApplication.java │ │ ├── config │ │ ├── AppInitConfig.java │ │ ├── AsyncThreadPoolConfig.java │ │ ├── CachingConfig.java │ │ ├── InitConfig.java │ │ ├── ServerInitConfig.java │ │ ├── WebConfig.java │ │ ├── auth │ │ │ ├── AuthWebConfig.java │ │ │ └── RestTemplateAuthInterceptor.java │ │ └── demo │ │ │ └── DemoInit.java │ │ ├── controller │ │ ├── CacheExampleController.java │ │ ├── DemoController.java │ │ ├── HttpController.java │ │ ├── ServerHealthController.java │ │ ├── SqlExampleController.java │ │ └── UserController.java │ │ ├── model │ │ ├── WebObjMapper.java │ │ ├── param │ │ │ └── UserQueryParam.java │ │ └── vo │ │ │ ├── CacheExampleVo.java │ │ │ └── UserVo.java │ │ └── service │ │ ├── AopExampleService.java │ │ ├── CacheExampleService.java │ │ └── SqlExampleService.java └── resources │ ├── ValidationMessages.properties │ ├── ValidationMessages_zh_CN.properties │ ├── application.yml │ ├── db │ └── migration │ │ └── V2019.12.24.1__baseline.sql │ ├── log4j2-example.xml │ ├── log4j2.xml │ ├── messages.properties │ └── messages_zh_CN.properties └── test └── java └── top └── rizon └── springbestpractice ├── AppTest.java └── web └── DemoApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/** 4 | !**/src/test/** 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | 29 | ### VS Code ### 30 | .vscode/ 31 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.5"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/othorizon/spring-best-practices/2f96434f8a66c99c569214d7d2d1b643f473fa26/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar 3 | -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 7 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/maven-plugin/) 8 | * [Spring cache abstraction](https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#boot-features-caching) 9 | * [Spring Configuration Processor](https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#configuration-metadata-annotation-processor) 10 | * [Spring Web](https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications) 11 | * [Flyway Migration](https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#howto-execute-flyway-database-migrations-on-startup) 12 | 13 | ### Guides 14 | The following guides illustrate how to use some features concretely: 15 | 16 | * [Caching Data with Spring](https://spring.io/guides/gs/caching/) 17 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 18 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 19 | * [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring 最佳实践 2 | 3 | 总结了本人多年Java开发中的一些开发经验以及工具类和Spring框架的应用 4 | 采用了项目Demo的方式把零散的内容联系在一起去展示其用法,可以直接拿来作为种子项目,用于快速构建中小型的spring-boot项目 5 | **项目持续更新中,将会逐步扩充完善** 6 | 7 | Spring Boot版本 :2.1.7 , 兼容的Spring Cloud版本为 Greenwich ,版本对照参考[官网Overview](https://spring.io/projects/spring-cloud#overview) 8 | 9 | ## 概要 10 | 11 | - 如何配置拦截器:interceptor、filter、@RestControllerAdvice 12 | - bean的初始化: InitializingBean接口、@conditionXXX 注解 13 | - 如何获取applicationContext上下文: ApplicationContextAware 14 | - 枚举的优雅使用: 1、如何把枚举作为接口的交互参数:@JsonCreator、@JsonValue ,要注意fastjson和spring采用的jackson 对注解的支持 2、valueOfByXX 15 | - 缓存的优雅使用: @Cacheable 、CaffeineCacheManager、请求级别的缓存RequestScopedCacheManager,注意防止副作用操作污染缓存数据 16 | - 配置文件配置时间属性:java.time.Duration 17 | - 正确的报错方式,message的国际化: [参考](https://www.jianshu.com/p/4d5f16f6ab82) 18 | - 接口参数校验:@Validated、javax.validation.constraints、自定义参数校验注解 19 | - 日志的优雅配置:log4j与logback的基础、使用MDC增加tractId跟踪日志 20 | - AOP的应用:自定义注解、方法拦截 21 | - java8 stream 的使用技巧 22 | 23 | 第三方工具的使用 24 | 25 | - json的处理:gson、fastjson、jackson,json-path 26 | - RestTemplate的优雅使用:工具类的封装、header的注入 27 | - 借助Mybatis-Plus实现零SQL开发 28 | - 借助MapStruct实现po、bo、vo等对象之间的转换 29 | - 健康接口,项目部署版本检查: buildnumber-maven-plugin 30 | - 如何深度复制对象:json复制、mapStruct、BeanUtils 31 | - apache-common 系列、hutool等基本工具类 32 | - 自定义的时间格式化工具类、jackson的封装工具 33 | 34 | 运维 35 | 36 | - 数据库版本维护 flyway 37 | - CI/CD相关: Jenkins的常用配置方式、docker、kubernetes 38 | 39 | ### 在项目中的实践 40 | 41 | - 项目必备的工具包:apache-common系列、gson 等 42 | - 借助@RestControllerAdvice实现全局统一的response返回,方法直接通过抛异常来返回 43 | - 更好的实现项目的初始化相关操作 44 | 45 | ### 代码之道 46 | 47 | - 如何干掉if-else 48 | 49 | ## 导航 50 | 51 | #### 健康接口 52 | 53 | [web/src/main/java/top/rizon/springbestpractice/web/controller/ServerHealthController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/ServerHealthController.java) 54 | 55 | 健康接口是项目必备接口 56 | 基本的ping-pong:接口作为最基本的服务存活检测 57 | 服务器信息接口:可以打印服务常用信息的接口,比如服务器时间等 58 | git版本信息的接口:接口打印了git版本号和build时间,在开发联调期间是非常重要的一个运维参考 59 | _ps. 要从auth认证拦截中排除_ 60 | 61 | #### 接口参数校验 62 | 63 | [web/src/main/java/top/rizon/springbestpractice/web/controller/UserController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/UserController.java) 64 | 错误提示配置:[web/src/main/resources/ValidationMessages.properties](web/src/main/resources/ValidationMessages.properties) 65 | 66 | 示例方法:`top.rizon.springbestpractice.web.controller.UserController.list` 67 | 68 | 通过注解实现参数校验, 69 | 错误提示信息可以使用配置文件实现国际化处理 70 | 71 | #### 缓存的几种写法 72 | 73 | [web/src/main/java/top/rizon/springbestpractice/web/controller/CacheExampleController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/CacheExampleController.java) 74 | 75 | #### 登陆认证的简单实现方式 76 | 77 | [web/src/main/java/top/rizon/springbestpractice/web/config/auth/AuthWebConfig.java](web/src/main/java/top/rizon/springbestpractice/web/config/auth/AuthWebConfig.java) 78 | 79 | #### 全局异常拦截处理 80 | 81 | [common/src/main/java/top/rizon/springbestpractice/common/handler/ExceptionHandlers.java](common/src/main/java/top/rizon/springbestpractice/common/handler/ExceptionHandlers.java) 82 | 83 | #### http请求工具RestTemplate的简单封装使用 84 | 85 | [common/src/main/java/top/rizon/springbestpractice/common/utils/http/SimpleRestTemplateUtils.java](common/src/main/java/top/rizon/springbestpractice/common/utils/http/SimpleRestTemplateUtils.java) 86 | RestTemplate拦截器:RestTemplateAuthConfig、BaseAuthHeaderHttpRequestInterceptor 87 | 88 | #### 封装的工具类 89 | 90 | [common/src/main/java/top/rizon/springbestpractice/common/utils/](common/src/main/java/top/rizon/springbestpractice/common/utils/) 91 | 92 | ##### sql注入的应用 93 | 94 | [dao/src/main/java/top/rizon/springbestpractice/dao/utils/dynamictblname/](dao/src/main/java/top/rizon/springbestpractice/dao/utils/dynamictblname/) 95 | `top.rizon.springbestpractice.web.controller.SqlExampleController.queryDateTable` 96 | 动态表名,一般可用于按日期等方式做分表的业务场景 97 | 顺便演示了PageHelper的分页工具的使用 98 | 99 | #### java8 stream 的技巧 100 | 101 | [common/src/test/java/top/rizon/springbestpractice/common/utils/StreamUtilTest.java](common/src/test/java/top/rizon/springbestpractice/common/utils/StreamUtilTest.java) 102 | 103 | JDK8引入的Lambda表达式和Stream为Java平台提供了函数式编程的支持,java提供了consumer、function等一系列接口为函数式编程提供了基础。 104 | Lambda表达式是一个能够作为参数传递的匿名函数对象,它没有名字,有参数列表、函数体、返回类型,也可以抛出异常。它的类型是函数接口(Functional Interface)。 105 | 函数式编程以操作(函数)为中心,强调变量不变性,无副作用。 106 | 107 | #### 第三方工具类相关 108 | 109 | MapStruct java对象映射工具 110 | [web/src/main/java/top/rizon/springbestpractice/web/model/WebObjMapper.java](web/src/main/java/top/rizon/springbestpractice/web/model/WebObjMapper.java) 111 | ps. 某些场景下用于deepClone也是一个不错的选择 112 | 113 | #### AOP的应用案例 114 | 115 | [common/src/main/java/top/rizon/springbestpractice/common/aspect/](common/src/main/java/top/rizon/springbestpractice/common/aspect/) 116 | 117 | ##### 分页页码自动修复 118 | 119 | [web/src/main/java/top/rizon/springbestpractice/web/controller/UserController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/UserController.java) 120 | `top.rizon.springbestpractice.web.controller.UserController.list` 121 | 当页码大于数据真实页码时会纠正为最后一页的数据,这可以解决前端分页展示删除最后一页的最后一条数据时刷新后为无数据的空白页的问题 122 | 123 | ##### 打印方法执行时间 124 | 125 | [web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java) 126 | `top.rizon.springbestpractice.web.service.AopExampleService.sleepMethod` 127 | 打印方法执行时间,这在优化代码,排查耗时过高的方法时有一定的帮助 128 | 129 | #### 代码之道 130 | 131 | ##### 策略模式 干掉if-else 132 | 133 | [web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java) 134 | `top.rizon.springbestpractice.web.controller.DemoController.formatDate` 135 | 如果你的if-else过于复杂那么应当考虑抽象业务了,简单的业务可以直接用枚举写抽象方法的实现,复杂的业务则可以写接口类去实现 136 | 137 | #### 使用枚举作为请求参数 138 | 139 | [web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java](web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java) 140 | `top.rizon.springbestpractice.web.controller.DemoController.formatDate` 141 | jackson的`@JsonCreator`可以指定json反序列化时的构造函数,`@JsonValue`则可以指定对象序列化时的取值属性 142 | 143 | ## 使用 144 | 145 | [API Doc - Postman](https://documenter.getpostman.com/view/494976/SWLZgAn8) 146 | 147 | ### 构建部署 148 | 149 | #### 编译jar 150 | 151 | ```bash 152 | # buildnumber-maven-plugin配置所致,在clean之后运行测试用例会由于尚未替换的配置导致配置文件读取失败 153 | mvn clean package -DskipTests=true 154 | java -jar web/target/web-0.0.1-SNAPSHOT.jar 155 | ``` 156 | 157 | #### 将外部配置文件和jar一起打包成tar 158 | 159 | ```bash 160 | mvn clean package -DbuildType=tar -DpackageConf=true -DconfEnv=test -DskipTests=true 161 | # 编译会生成tar: web/target/spring-best-practice-web-0.0.1-SNAPSHOT.tar.gz 162 | # tar包中包含了启动脚本 解压到目标位置后执行启动脚本 163 | sh bin/app.sh start|stop|restart|status|pid 164 | ``` 165 | 166 | #### 构建docker镜像 167 | 168 | ```bash 169 | mvn clean package -DbuildType=docker -DpackageConf=true -DconfEnv=test -DskipTests=true 170 | docker run --rm -it -p8080:8080 rizon/spring-best-practice 171 | ``` 172 | 173 | ### Online IDE 174 | 175 | [![在 Gitpod 中打开](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io#https://github.com/othorizon/spring-best-practices) 176 | -------------------------------------------------------------------------------- /auth/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | spring-best-practice 7 | top.rizon.springbestpractice 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | auth 12 | 13 | 14 | 15 | 16 | top.rizon.springbestpractice 17 | common 18 | ${project.version} 19 | 20 | 21 | top.rizon.springbestpractice 22 | dao 23 | ${project.version} 24 | 25 | 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-compiler-plugin 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /auth/src/main/java/top/rizon/springbestpractice/auth/AuthObjMapper.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.auth; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.factory.Mappers; 5 | import top.rizon.springbestpractice.dao.po.User; 6 | 7 | /** 8 | * @author Rizon 9 | * @date 2019/12/25 10 | */ 11 | @Mapper(componentModel = "spring") 12 | public interface AuthObjMapper { 13 | AuthObjMapper INSTANCE = Mappers.getMapper(AuthObjMapper.class); 14 | 15 | /** 16 | * 将用户转换为authUser 17 | * 18 | * @param user 19 | * @return 20 | */ 21 | SimpleAuthUser toAuthUser(User user); 22 | } 23 | -------------------------------------------------------------------------------- /auth/src/main/java/top/rizon/springbestpractice/auth/AuthService.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.auth; 2 | 3 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.cache.annotation.Cacheable; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.stereotype.Service; 9 | import top.rizon.springbestpractice.dao.helper.UserHelper; 10 | import top.rizon.springbestpractice.dao.po.User; 11 | 12 | /** 13 | * @author Rizon 14 | * @date 2019/12/25 15 | */ 16 | @Service 17 | @RequiredArgsConstructor 18 | @Slf4j 19 | public class AuthService { 20 | private final UserHelper userHelper; 21 | 22 | @Cacheable(value = "queryUserCacheable") 23 | @Nullable 24 | public User queryUserCacheable(String token) { 25 | return userHelper.getOne(Wrappers.lambdaQuery() 26 | .eq(User::getToken, token)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /auth/src/main/java/top/rizon/springbestpractice/auth/AuthenticationInterceptor.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.auth; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.stereotype.Component; 6 | import top.rizon.springbestpractice.common.exception.AuthFailedException; 7 | import top.rizon.springbestpractice.common.handler.AbstractAuthHandler; 8 | import top.rizon.springbestpractice.common.utils.AuthUtil; 9 | import top.rizon.springbestpractice.dao.po.User; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | /** 15 | * @author Rizon 16 | * @date 2019/12/25 17 | */ 18 | @Component 19 | @RequiredArgsConstructor 20 | public class AuthenticationInterceptor extends AbstractAuthHandler { 21 | private static final String AUTH_HEADER = "AUTH-TOKEN"; 22 | private final AuthService authService; 23 | private final AuthObjMapper authObjMapper; 24 | 25 | @Override 26 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 27 | String token = request.getHeader(AUTH_HEADER); 28 | if (StringUtils.isEmpty(token)) { 29 | throw new AuthFailedException("NOT_FOUND_AUTH_TOKEN",AUTH_HEADER); 30 | } 31 | User user = authService.queryUserCacheable(token); 32 | if (user == null) { 33 | throw new AuthFailedException(); 34 | } 35 | AuthUtil.setAuthUser(request, authObjMapper.toAuthUser(user)); 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /auth/src/main/java/top/rizon/springbestpractice/auth/SimpleAuthUser.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.auth; 2 | 3 | import lombok.Data; 4 | import top.rizon.springbestpractice.common.model.dto.AuthUser; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/25 9 | */ 10 | @Data 11 | public class SimpleAuthUser implements AuthUser { 12 | private Long id; 13 | private String name; 14 | private String token; 15 | 16 | @Override 17 | public long getId() { 18 | return id; 19 | } 20 | 21 | @Override 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | @Override 27 | public String getToken() { 28 | return token; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /auth/src/test/java/top/rizon/springbestpractice/AppTest.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest 11 | { 12 | /** 13 | * Rigorous Test :-) 14 | */ 15 | @Test 16 | public void shouldAnswerWithTrue() 17 | { 18 | assertTrue( true ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | spring-best-practice 7 | top.rizon.springbestpractice 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | common 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/aspect/AutoRepairPageAspect.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.aspect; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.collections4.CollectionUtils; 5 | import org.apache.commons.lang3.reflect.FieldUtils; 6 | import org.aspectj.lang.ProceedingJoinPoint; 7 | import org.aspectj.lang.annotation.Around; 8 | import org.aspectj.lang.annotation.Aspect; 9 | import org.aspectj.lang.annotation.Pointcut; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.stereotype.Component; 12 | import top.rizon.springbestpractice.common.model.response.PageResponse; 13 | 14 | import java.lang.annotation.*; 15 | import java.lang.reflect.Field; 16 | import java.util.Collection; 17 | 18 | /** 19 | *

分页查询的切片处理

20 | * 只对controller包下的类生效 21 | *

当分页查询的页码参数大于最后一页,则改为最后一页的页码重新查询数据

22 | *

使用配置 {@code base-server.auto-repair-page} 全局禁用该功能

23 | *

使用注解 {@link DisableAutoRepairPage} 在方法上禁用该功能

24 | * 25 | *

26 | * 其实spring对于aop的实现是通过动态代理(jdk的动态代理或者cglib的动态代理), 27 | * 它只是使用了aspectJ的Annotation,并没有使用它的编译期和织入器, 28 | * 所以受限于这点,有些增强就做不到,比如 调用自己的方法就无法走代理 29 | *

30 | *

31 | * 参考: 32 | * AspectJ语法:https://blog.csdn.net/sunlihuo/article/details/52701548 33 | * AspectJ与Spring Aop的区别:https://zhuanlan.zhihu.com/p/50612298 34 | * 35 | * @author Rizon 36 | * @date 2019-09-25 37 | */ 38 | @Slf4j 39 | @Aspect 40 | @Component 41 | @ConditionalOnProperty(matchIfMissing = true, value = "base-server.auto-repair-page", havingValue = "true") 42 | public class AutoRepairPageAspect { 43 | 44 | /** 45 | * className+ 表示匹配子类型 46 | */ 47 | @Pointcut("execution(top.rizon.springbestpractice.common.model.response.Response+ top.rizon.springbestpractice..controller..*(..))" + 48 | " && !@annotation(top.rizon.springbestpractice.common.aspect.AutoRepairPageAspect.DisableAutoRepairPage)") 49 | public void methodPointcut() { 50 | } 51 | 52 | @Around("methodPointcut()") 53 | public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { 54 | Object result = joinPoint.proceed(); 55 | try { 56 | //只有返回的是PageResponseParam 才会处理 57 | if (!(result instanceof PageResponse)) { 58 | return result; 59 | } 60 | PageResponse pageResponse = (PageResponse) result; 61 | /* 62 | 不进行空数据的判断,只判断页码 63 | if (!hasEmptyData(pageResponse)) { 64 | return result; 65 | } 66 | */ 67 | if (pageResponse.getPagination() == null) { 68 | return result; 69 | } 70 | if (pageResponse.getPagination().getTotalPage() < 1) { 71 | //当总页码为0时,则设置当前页码为1 并直接返回结果 72 | pageResponse.getPagination().setPage(1); 73 | return result; 74 | } 75 | if (pageResponse.getPagination().getPage() <= pageResponse.getPagination().getTotalPage()) { 76 | return result; 77 | } 78 | //如果页码是超过了最后一页那么将页面改为最后一页 重新执行方法 79 | 80 | //设置新的页码 因为response中的pageParam对象和参数中的pageParam对象是同一个对象,所以 81 | pageResponse.getPagination().setPage((int) pageResponse.getPagination().getTotalPage()); 82 | //重新执行方法 83 | log.info("page greatThan totalPage, reset page and reProceed method"); 84 | return joinPoint.proceed(); 85 | } catch (Exception ex) { 86 | log.error("aop process PageResponseParam failed", ex); 87 | } 88 | return result; 89 | } 90 | 91 | /** 92 | * 是否包含任意一个 使用了tag标记的空值属性 93 | * 94 | * @return 95 | */ 96 | private boolean hasEmptyData(Object dataObj) throws IllegalAccessException { 97 | for (Field field : FieldUtils.getFieldsListWithAnnotation(dataObj.getClass(), ResDataTag.class)) { 98 | 99 | ResDataTag tag = field.getAnnotation(ResDataTag.class); 100 | Object value = FieldUtils.readField(field, dataObj, true); 101 | 102 | if (tag.nullAsEmpty() && value == null) { 103 | return true; 104 | } 105 | if (value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) { 106 | return true; 107 | } 108 | //递归查看 109 | if (hasEmptyData(value)) { 110 | return true; 111 | } 112 | } 113 | return false; 114 | } 115 | 116 | @Target(ElementType.FIELD) 117 | @Retention(RetentionPolicy.RUNTIME) 118 | public @interface ResDataTag { 119 | /** 120 | * 如果值是null是否也看作返回了空对象去重新计算页面查询数据 121 | * 默认false 122 | * 不推荐使用null作为判断条件 123 | */ 124 | boolean nullAsEmpty() default false; 125 | } 126 | 127 | /** 128 | * 禁用自动修复页码功能 129 | */ 130 | @Target(ElementType.METHOD) 131 | @Retention(RetentionPolicy.RUNTIME) 132 | @Inherited 133 | public @interface DisableAutoRepairPage { 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/aspect/MethodLogAspect.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.aspect; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.aspectj.lang.ProceedingJoinPoint; 5 | import org.aspectj.lang.annotation.AfterReturning; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.annotation.Pointcut; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * 拦截方法 打印执行日志 14 | * @author Rizon 15 | * @date 2019-03-18 16 | */ 17 | @Slf4j 18 | @Aspect 19 | @ConditionalOnProperty(value="dev.log.method-process", havingValue = "true" ,matchIfMissing = true) 20 | @Component 21 | public class MethodLogAspect { 22 | 23 | @Pointcut("execution(* top.rizon.springbestpractice..AopExampleService.*(..)) && !bean(methodLogAspect)") 24 | public void methodPointcut() { 25 | } 26 | 27 | 28 | @Around("methodPointcut()") 29 | public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { 30 | long start = System.currentTimeMillis(); 31 | try { 32 | Object result = joinPoint.proceed(); 33 | long end = System.currentTimeMillis(); 34 | log.info("执行" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName() 35 | + "方法," + parseParams(joinPoint.getArgs()) + ",耗时:" + (end - start) + " ms!"); 36 | return result; 37 | } catch (Throwable e) { 38 | long end = System.currentTimeMillis(); 39 | log.error(joinPoint + ",耗时:" + (end - start) + " ms,抛出异常 :" + e.getMessage()); 40 | throw e; 41 | } 42 | } 43 | 44 | @AfterReturning(returning = "ret", pointcut = "methodPointcut()") 45 | public void doAfterReturning(Object ret) throws Throwable { 46 | log.info("返回值:" + ret); 47 | } 48 | 49 | private String parseParams(Object[] parames) { 50 | 51 | if (null == parames || parames.length <= 0) { 52 | return "该方法没有参数"; 53 | 54 | } 55 | StringBuilder param = new StringBuilder("请求参数 # 个:[ "); 56 | int i = 0; 57 | for (Object obj : parames) { 58 | i++; 59 | if (i == 1) { 60 | param.append(obj.toString()); 61 | continue; 62 | } 63 | param.append(" ,").append(obj.toString()); 64 | } 65 | return param.append(" ]").toString().replace("#", String.valueOf(i)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/cache/CacheHandlerInterceptor.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.cache; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | /** 10 | * 清理 RequestScopedCacheManager的缓存 11 | * 12 | * @author Rizon 13 | * @date 2019/12/13 14 | */ 15 | @RequiredArgsConstructor 16 | public class CacheHandlerInterceptor extends HandlerInterceptorAdapter { 17 | 18 | private final RequestScopedCacheManager requestScopedCacheManager; 19 | 20 | 21 | @Override 22 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 23 | requestScopedCacheManager.clearCaches(); 24 | return true; 25 | } 26 | 27 | /** 28 | * afterCompletion与postHandler不同,即使抛出异常后也会被执行 29 | */ 30 | @Override 31 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 32 | requestScopedCacheManager.clearCaches(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/cache/RequestScopedCacheManager.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.cache; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.springframework.cache.Cache; 5 | import org.springframework.cache.CacheManager; 6 | import org.springframework.cache.concurrent.ConcurrentMapCache; 7 | 8 | import java.util.Collection; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | /** 13 | * request请求域的缓存工具 14 | * 15 | * @author Rizon 16 | * @date 2019/12/13 17 | */ 18 | public class RequestScopedCacheManager implements CacheManager { 19 | 20 | private static final ThreadLocal> THREAD_LOCAL_CACHE = ThreadLocal.withInitial(ConcurrentHashMap::new); 21 | 22 | @Override 23 | public Cache getCache(@NotNull String name) { 24 | final Map cacheMap = THREAD_LOCAL_CACHE.get(); 25 | return cacheMap.computeIfAbsent(name, this::createCache); 26 | } 27 | 28 | private Cache createCache(String name) { 29 | return new ConcurrentMapCache(name); 30 | } 31 | 32 | @NotNull 33 | @Override 34 | public Collection getCacheNames() { 35 | return THREAD_LOCAL_CACHE.get().keySet(); 36 | } 37 | 38 | /** 39 | * threadLocal使用必须主动remove防止内存泄漏 40 | */ 41 | public void clearCaches() { 42 | THREAD_LOCAL_CACHE.remove(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/constant/DateFormatType.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.constant; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import top.rizon.springbestpractice.common.exception.BaseServerException; 8 | import top.rizon.springbestpractice.common.utils.DataDate; 9 | import top.rizon.springbestpractice.common.utils.DateTimeUtils; 10 | 11 | import java.util.Arrays; 12 | 13 | /** 14 | * 借助枚举实现的策略模式,避免过于复杂的if-else的代码 15 | * 16 | * @author Rizon 17 | * @date 2019/12/26 18 | */ 19 | @Getter 20 | @AllArgsConstructor 21 | public enum DateFormatType { 22 | /**/ 23 | DATE("DATE", 0) { 24 | @Override 25 | public String format(DataDate dataDate) { 26 | return dataDate.toString(DateTimeUtils.YYYYMMDD); 27 | } 28 | }, 29 | DATE_TIME("DATE_TIME", 1) { 30 | @Override 31 | public String format(DataDate dataDate) { 32 | return dataDate.toString(DateTimeUtils.YYYYMMDDHHMMSS); 33 | } 34 | }, 35 | TIME("TIME", 2) { 36 | @Override 37 | public String format(DataDate dataDate) { 38 | return dataDate.toString(DateTimeUtils.HHMMSS); 39 | } 40 | }; 41 | private String code; 42 | @JsonValue 43 | private Integer type; 44 | 45 | /** 46 | * 格式化日期 47 | */ 48 | public abstract String format(DataDate dataDate); 49 | 50 | @JsonCreator 51 | public static DateFormatType valueOfByType(Integer type) { 52 | return Arrays.stream(DateFormatType.values()) 53 | .filter(t -> t.getType().equals(type)) 54 | .findAny() 55 | .orElseThrow(() -> new BaseServerException("error DateFormatType:" + type)); 56 | } 57 | 58 | /** 59 | * 即使参数为int类型,requestBody转换时依然会按照string处理 60 | * 61 | * @param type 62 | * @return 63 | */ 64 | @JsonCreator 65 | public static DateFormatType valueOfByType(String type) { 66 | return valueOfByType(type == null ? null : Integer.parseInt(type)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/exception/AssertFailExceptionBase.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.exception; 2 | 3 | /** 4 | * @author Rizon 5 | * @date 2019/1/24 6 | */ 7 | public class AssertFailExceptionBase extends BaseServerException { 8 | public AssertFailExceptionBase() { 9 | } 10 | 11 | public AssertFailExceptionBase(String message) { 12 | super(message); 13 | } 14 | 15 | public AssertFailExceptionBase(String message, Object... args) { 16 | super(message, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/exception/AuthFailedException.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.exception; 2 | 3 | /** 4 | * @author Rizon 5 | * @date 2019/12/2 6 | */ 7 | public class AuthFailedException extends BaseServerException{ 8 | public AuthFailedException() { 9 | } 10 | 11 | public AuthFailedException(String message, Object... args) { 12 | super(message, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/exception/BaseServerException.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.experimental.Accessors; 6 | 7 | /** 8 | * 基础异常类 9 | * 10 | * @author Rizon 11 | * @date 2019/1/24 12 | */ 13 | public class BaseServerException extends RuntimeException { 14 | /** 15 | * 自定义的拼接在Response的errMessage前面的错误信息 16 | * 不会展示在页面上 17 | */ 18 | @Accessors(chain = true) 19 | @Setter 20 | @Getter 21 | private String errMessage; 22 | 23 | /** 24 | * message 参数 25 | */ 26 | @Accessors(chain = true) 27 | @Setter 28 | @Getter 29 | private Object[] msgArgs; 30 | 31 | public BaseServerException() { 32 | } 33 | 34 | public BaseServerException(String message, Object... args) { 35 | super(String.format(message, args)); 36 | this.msgArgs = args; 37 | } 38 | 39 | public BaseServerException(String message, Throwable cause) { 40 | super(message, cause); 41 | } 42 | 43 | public BaseServerException(Throwable cause, String message, Object... args) { 44 | super(String.format(message, args), cause); 45 | this.msgArgs = args; 46 | } 47 | 48 | public BaseServerException(Throwable cause) { 49 | super(cause); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/handler/AbstractAuthHandler.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.handler; 2 | 3 | import org.springframework.web.servlet.HandlerInterceptor; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | /** 9 | * @author Rizon 10 | * @date 2019/12/3 11 | */ 12 | public abstract class AbstractAuthHandler implements HandlerInterceptor { 13 | @Override 14 | public abstract boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/handler/ExceptionHandlers.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.handler; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.http.converter.HttpMessageNotReadableException; 11 | import org.springframework.validation.BindException; 12 | import org.springframework.web.HttpRequestMethodNotSupportedException; 13 | import org.springframework.web.bind.MethodArgumentNotValidException; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | import org.springframework.web.bind.annotation.RestControllerAdvice; 16 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 17 | import top.rizon.springbestpractice.common.exception.AssertFailExceptionBase; 18 | import top.rizon.springbestpractice.common.exception.AuthFailedException; 19 | import top.rizon.springbestpractice.common.exception.BaseServerException; 20 | import top.rizon.springbestpractice.common.model.response.Response; 21 | import top.rizon.springbestpractice.common.utils.ExceptionUtils; 22 | 23 | import java.sql.SQLException; 24 | import java.util.Objects; 25 | 26 | /** 27 | * @author Rizon 28 | * @date 2019/12/13 29 | */ 30 | @RestControllerAdvice 31 | @Slf4j 32 | @RequiredArgsConstructor 33 | public class ExceptionHandlers { 34 | private final MessageSourceHandler messageSourceHandler; 35 | 36 | public static String ERROR = "错误 "; 37 | public static String SQL_ERROR = "sql异常 "; 38 | public static String HTTP_ERROR = "http异常 "; 39 | public static String PARAMS_ERROR = "参数校验错误 "; 40 | public static String ILLEGAL_PARAMS_ERROR = "非法参数异常 "; 41 | 42 | private String appendMsg(String key, Throwable throwable) { 43 | return key + ": " + throwable; 44 | } 45 | 46 | private String appendMsg(String key, String msg) { 47 | return key + ": " + msg; 48 | } 49 | 50 | @ExceptionHandler(Exception.class) 51 | public Response exceptionHandler(Exception e) { 52 | log.error(ERROR, e); 53 | return Response.failure(e.getMessage(), appendMsg(ERROR, ExceptionUtils.rootCauseToString(e)), null); 54 | } 55 | 56 | @ExceptionHandler(SQLException.class) 57 | public Response sqlExceptionHandler(Exception e) { 58 | log.error(SQL_ERROR, e); 59 | return Response.failure(e.getMessage(), appendMsg(SQL_ERROR, ExceptionUtils.rootCauseToString(e)), null); 60 | } 61 | 62 | 63 | /** 64 | * http参数异常 65 | * 66 | * @param e 67 | * @return 68 | */ 69 | @ExceptionHandler({HttpMessageNotReadableException.class, HttpRequestMethodNotSupportedException.class, MethodArgumentTypeMismatchException.class}) 70 | public Response httpMessageNotReadableExceptionHandler(Exception e) { 71 | log.error(HTTP_ERROR, e); 72 | return Response.failure(e.getMessage(), appendMsg(HTTP_ERROR, ExceptionUtils.rootCauseToString(e)), null); 73 | } 74 | 75 | /** 76 | * get参数验证异常 valid异常 77 | * 78 | * @param e 79 | * @return 80 | */ 81 | @ExceptionHandler({BindException.class}) 82 | public Response validateExceptionHandler(BindException e) { 83 | String defaultMessage = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); 84 | FailedMessage msg = getMessage(defaultMessage, null); 85 | log.error(buildErrorMsg(msg.getMessage(), e.getMessage()), e); 86 | return getFailedResponse(msg, PARAMS_ERROR, e); 87 | } 88 | 89 | /** 90 | * post参数验证异常 valid异常 91 | * 92 | * @param e 93 | * @return 94 | */ 95 | @ExceptionHandler({MethodArgumentNotValidException.class}) 96 | public Response validateExceptionHandler(MethodArgumentNotValidException e) { 97 | String defaultMessage = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); 98 | FailedMessage msg = getMessage(defaultMessage, null); 99 | log.error(buildErrorMsg(msg.getMessage(), e.getMessage()), e); 100 | return getFailedResponse(msg, PARAMS_ERROR, e); 101 | } 102 | 103 | 104 | /** 105 | * 自定义校验异常 106 | */ 107 | @ExceptionHandler(AssertFailExceptionBase.class) 108 | public Response assertExceptionHandler(AssertFailExceptionBase e) { 109 | FailedMessage msg = getMessage(e.getMessage(), e.getMsgArgs()); 110 | log.error(buildErrorMsg(msg, e), e); 111 | return getFailedResponse(msg, e.getErrMessage(), e); 112 | } 113 | 114 | /** 115 | * 认证失败异常 116 | */ 117 | @ExceptionHandler(AuthFailedException.class) 118 | public ResponseEntity authFailedExceptionHandler(AuthFailedException e) { 119 | FailedMessage msg = getMessage(e.getMessage(), e.getMsgArgs()); 120 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(msg.getMessage()); 121 | } 122 | 123 | /** 124 | * 自定义异常 125 | * 126 | * @param e 127 | * @return 128 | */ 129 | @ExceptionHandler(BaseServerException.class) 130 | public Response baseServerExceptionHandler(BaseServerException e) { 131 | FailedMessage msg = getMessage(e.getMessage(), e.getMsgArgs()); 132 | log.error(buildErrorMsg(msg, e), e); 133 | return getFailedResponse(msg, e.getErrMessage(), e); 134 | } 135 | 136 | 137 | /** 138 | * 非法参数异常 139 | * 140 | * @param e 141 | * @return 142 | */ 143 | @ExceptionHandler(IllegalArgumentException.class) 144 | public Response baseServerExceptionHandler(IllegalArgumentException e) { 145 | FailedMessage msg = getMessage(e.getMessage(), null); 146 | log.error(buildErrorMsg(msg.getMessage(), e.getMessage()), e); 147 | return getFailedResponse(msg, PARAMS_ERROR, e); 148 | } 149 | 150 | 151 | private Response getFailedResponse(FailedMessage msg, String errMsg, Exception ex) { 152 | return new Response<>( 153 | msg.getCode(), 154 | msg.getMessage(), 155 | appendMsg(StringUtils.defaultString(errMsg), ExceptionUtils.rootCauseToString(ex)), null); 156 | } 157 | 158 | private FailedMessage getMessage(String msg, Object[] msgArgs) { 159 | return new FailedMessage(messageSourceHandler.getMessage(msg, msgArgs, msg)); 160 | } 161 | 162 | private String buildErrorMsg(String originMsg, String readableMsg) { 163 | return (Objects.equals(originMsg, readableMsg) ? 164 | originMsg : readableMsg + "(" + originMsg + ")"); 165 | } 166 | 167 | private String buildErrorMsg(FailedMessage msg, BaseServerException e) { 168 | StringBuilder sb = new StringBuilder(); 169 | if (Objects.equals(e.getMessage(), msg.getMessage())) { 170 | sb.append(e.getMessage()); 171 | } else { 172 | sb.append(msg.getMessage()).append("(").append(e.getMessage()).append(")"); 173 | } 174 | if (e.getErrMessage() != null) { 175 | sb.append("=>").append(e.getErrMessage()); 176 | } 177 | return sb.toString(); 178 | } 179 | 180 | @Getter 181 | @Setter 182 | public static class FailedMessage { 183 | private Integer code = Response.CODE_FAILURE; 184 | private String message; 185 | 186 | public FailedMessage(String fullMsg) { 187 | try { 188 | String[] split = fullMsg.split(":::", 2); 189 | if (split.length < 2) { 190 | this.message = fullMsg; 191 | } else { 192 | this.code = Integer.parseInt(split[0]); 193 | this.message = split[1]; 194 | } 195 | } catch (Exception ex) { 196 | this.code = Response.CODE_FAILURE; 197 | this.message = fullMsg; 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/handler/MessageSourceHandler.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.handler; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.MessageSource; 5 | import org.springframework.lang.Nullable; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.support.RequestContextUtils; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | 11 | /** 12 | * @author Rizon 13 | * @date 2019/12/13 14 | */ 15 | @Component 16 | @RequiredArgsConstructor 17 | public class MessageSourceHandler { 18 | private final HttpServletRequest request; 19 | private final MessageSource messageSource; 20 | 21 | public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage) { 22 | return messageSource.getMessage(code, args, defaultMessage, RequestContextUtils.getLocale(request)); 23 | } 24 | 25 | 26 | public String getMessage(String code, @Nullable Object[] args) { 27 | return messageSource.getMessage(code, args, RequestContextUtils.getLocale(request)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/handler/RestClientErrorExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.handler; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.client.HttpClientErrorException; 5 | import org.springframework.web.client.RestClientResponseException; 6 | import top.rizon.springbestpractice.common.exception.BaseServerException; 7 | 8 | /** 9 | * @author Rizon 10 | * @date 2019/12/13 11 | */ 12 | @Slf4j 13 | public class RestClientErrorExceptionHandler { 14 | 15 | /** 16 | * 处理异常 17 | * 18 | * @param ex 19 | */ 20 | public R doHandler(RestClientResponseException ex) { 21 | if (ex instanceof HttpClientErrorException.Unauthorized) { 22 | throw new BaseServerException("request third-part server auth failed") 23 | .setErrMessage("request third-part server auth failed:" + ex); 24 | } 25 | throw new BaseServerException("request failed:%s", ex.getResponseBodyAsString()).setErrMessage("request failed:" + ex); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/model/dto/AuthUser.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.model.dto; 2 | 3 | /** 4 | * 认证模块采用接口形式,这样可以方便的替换认证服务和屏蔽实际认证业务的内部逻辑 5 | * 6 | * @author Rizon 7 | * @date 2019/12/3 8 | */ 9 | public interface AuthUser { 10 | /** 11 | * 用户Id 12 | * 13 | * @return id 14 | */ 15 | long getId(); 16 | 17 | /** 18 | * 用户名 19 | * 20 | * @return name 21 | */ 22 | String getName(); 23 | 24 | /** 25 | * 登陆token 26 | * 27 | * @return 28 | */ 29 | String getToken(); 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/model/request/BaseReqParam.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.model.request; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.Valid; 6 | 7 | /** 8 | * 公共请求参数 9 | * 10 | * @author Rizon 11 | * @date 2019-02-13 12 | */ 13 | @Data 14 | public class BaseReqParam { 15 | @Valid 16 | private PageParam pageParam = new PageParam(); 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/model/request/PageParam.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.model.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | import javax.validation.constraints.Min; 9 | 10 | /** 11 | * @author Rizon 12 | * @date 2019/12/3 13 | */ 14 | @Data 15 | @Accessors(chain = true) 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class PageParam { 19 | /** 20 | * 当前页 21 | */ 22 | @Min(value =1,message = "{min.page}") 23 | private int page = 1; 24 | 25 | /** 26 | * 页大小 27 | */ 28 | @Min(value =1,message = "{min.pageSize}") 29 | private int pageSize = 10; 30 | 31 | /** 32 | * 总页数 33 | */ 34 | private long totalPage; 35 | 36 | /** 37 | * 总记录数 38 | */ 39 | private long totalRecord; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/model/response/PageResponse.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.model.response; 2 | 3 | 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | import top.rizon.springbestpractice.common.model.request.PageParam; 9 | 10 | /** 11 | * @author Rizon 12 | * @date 2019/12/3 13 | */ 14 | @EqualsAndHashCode(callSuper = true) 15 | @Data 16 | @Accessors(chain = true) 17 | @NoArgsConstructor 18 | public class PageResponse extends Response { 19 | private PageParam pagination; 20 | 21 | public PageResponse(PageParam pagination) { 22 | this.pagination = pagination; 23 | } 24 | 25 | public PageResponse(int code, String message, T data, PageParam pagination) { 26 | super(code, message, data); 27 | this.pagination = pagination; 28 | } 29 | 30 | public static PageResponse success(T data, PageParam pagination) { 31 | return new PageResponse<>(CODE_SUCCESS, MESSAGE_SUCCESS, data, pagination); 32 | } 33 | 34 | public static PageResponse success(String message, T data, PageParam pagination) { 35 | return new PageResponse<>(CODE_SUCCESS, message, data, pagination); 36 | } 37 | 38 | public static PageResponse failure(T data, PageParam pagination) { 39 | return new PageResponse<>(CODE_FAILURE, MESSAGE_FAILURE, data, pagination); 40 | } 41 | 42 | public static PageResponse failure(String message, T data, PageParam pagination) { 43 | return new PageResponse<>(CODE_FAILURE, message, data, pagination); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/model/response/Response.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.model.response; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | import org.apache.commons.lang3.StringUtils; 6 | import top.rizon.springbestpractice.common.exception.BaseServerException; 7 | import top.rizon.springbestpractice.common.utils.ResponseUtil; 8 | import top.rizon.springbestpractice.common.utils.TraceUtils; 9 | 10 | /** 11 | * @author Rizon 12 | * @date 2019/1/25 13 | */ 14 | @Data 15 | @Accessors(chain = true) 16 | public class Response { 17 | public static final Integer ERR_MESSAGE_LENGTH = 2048; 18 | public static final int CODE_SUCCESS = 200; 19 | public static final int CODE_FAILURE = 500; 20 | public static final int CODE_AUTH_FAILURE = 300; 21 | 22 | public static final String MESSAGE_SUCCESS = "成功"; 23 | public static final String MESSAGE_FAILURE = "失败"; 24 | 25 | private int status; 26 | private String message; 27 | 28 | /** 29 | * 生产环境中为了避免泄漏,关闭用户调试的错误信息 30 | */ 31 | private static boolean closeExStack; 32 | 33 | public static void setCloseExStack(boolean closeExStack) { 34 | Response.closeExStack = closeExStack; 35 | } 36 | 37 | private String errMessage; 38 | private String warnMsg; 39 | 40 | public String getRequestId() { 41 | return TraceUtils.getTraceId(); 42 | } 43 | 44 | public String getWarnMsg() { 45 | if (closeExStack) { 46 | return null; 47 | } 48 | return warnMsg == null ? ResponseUtil.popErrorMsg() : warnMsg; 49 | } 50 | 51 | public String getErrMessage() { 52 | if (closeExStack) { 53 | return null; 54 | } 55 | return StringUtils.substring(errMessage, 0, ERR_MESSAGE_LENGTH); 56 | } 57 | 58 | private T result; 59 | 60 | public Response() { 61 | 62 | } 63 | 64 | public Response(int status, String message, T result) { 65 | this.status = status; 66 | this.message = message; 67 | this.result = result; 68 | } 69 | 70 | public Response(int status, String message, String errMessage, T result) { 71 | this.status = status; 72 | this.message = message; 73 | this.result = result; 74 | this.errMessage = errMessage; 75 | } 76 | 77 | 78 | public static Response success(T data) { 79 | return new Response<>(CODE_SUCCESS, MESSAGE_SUCCESS, data); 80 | } 81 | 82 | public static Response success(String message, T data) { 83 | return new Response<>(CODE_SUCCESS, message, data); 84 | } 85 | 86 | public static Response failure(T data) { 87 | return new Response<>(CODE_FAILURE, MESSAGE_FAILURE, data); 88 | } 89 | 90 | public static Response failure(String message, T data) { 91 | return new Response<>(CODE_FAILURE, message, data); 92 | } 93 | 94 | public static Response failure(String message, String errMessage, T data) { 95 | return new Response<>(CODE_FAILURE, message, errMessage, data); 96 | } 97 | 98 | public static Response response(int status, String message, T data) { 99 | return new Response<>(status, message, null, data); 100 | } 101 | 102 | public static Response response(int status, String message, String errMessage, T data) { 103 | return new Response<>(status, message, errMessage, data); 104 | } 105 | 106 | public void setResult(T result) { 107 | this.result = result; 108 | } 109 | 110 | public T getResult() { 111 | return result; 112 | } 113 | 114 | public int getStatus() { 115 | return status; 116 | } 117 | 118 | public String getMessage() { 119 | return message; 120 | } 121 | 122 | public static void checkSuccess(Response response, boolean thrExpIfRetIsNull) { 123 | if (response.getStatus() != CODE_SUCCESS) { 124 | throw new BaseServerException("接口返回错误,status:%s,message:%s", response.getStatus(), response.getMessage()); 125 | } 126 | if (thrExpIfRetIsNull && response.getResult() == null) { 127 | throw new BaseServerException("接口返回的result是null"); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/Assert.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.jetbrains.annotations.Contract; 5 | import top.rizon.springbestpractice.common.exception.AssertFailExceptionBase; 6 | import top.rizon.springbestpractice.common.exception.BaseServerException; 7 | 8 | /** 9 | * 用于一些常见的断言 10 | * required* 相关的方法则可以在一行代码中完成验证和取值,更为简洁 11 | * 12 | * @author Rizon 13 | * @date 2018/9/11 14 | * @see com.google.common.base.Preconditions 15 | */ 16 | public class Assert { 17 | public static void isTrue(boolean test, String msg, Object... msgArgs) { 18 | if (!test) { 19 | throw new AssertFailExceptionBase(msg, msgArgs); 20 | } 21 | } 22 | 23 | public static void notEmpty(String str, String msg) { 24 | if (StringUtils.isEmpty(str)) { 25 | throw new AssertFailExceptionBase(msg); 26 | } 27 | } 28 | 29 | public static void notBlank(String str, String msg) { 30 | if (StringUtils.isBlank(str)) { 31 | throw new AssertFailExceptionBase(msg); 32 | } 33 | } 34 | 35 | 36 | @Contract("null, _, _ -> fail") 37 | public static void notNull(Object obj, String msg, Object... msgArgs) { 38 | if (null == obj) { 39 | throw new AssertFailExceptionBase(msg, msgArgs); 40 | } 41 | } 42 | 43 | @Contract("!null, _, _ -> !null; null, _, _ -> fail") 44 | public static T requiredNotNull(T obj, String msg, Object... msgArgs) { 45 | if (null != obj) { 46 | return obj; 47 | } else { 48 | throw new AssertFailExceptionBase(msg, msgArgs); 49 | } 50 | } 51 | 52 | @Contract("!null, _, _ -> !null; null, _, _ -> fail") 53 | public static String requiredNotEmpty(String obj, String msg, Object... msgArgs) { 54 | if (StringUtils.isNotEmpty(obj)) { 55 | return obj; 56 | } else { 57 | throw new AssertFailExceptionBase(msg, msgArgs); 58 | } 59 | } 60 | 61 | @Contract("!null, _, _ -> !null; null, _, _ -> fail") 62 | public static String requiredNotBlank(String obj, String msg, Object... msgArgs) { 63 | if (StringUtils.isNotBlank(obj)) { 64 | return obj; 65 | } else { 66 | throw new AssertFailExceptionBase(msg, msgArgs); 67 | } 68 | } 69 | 70 | /** 71 | * 期望只有一个或零个结果 72 | * 73 | * @param size 74 | */ 75 | public static void oneOrEmpty(int size) { 76 | if (size > 1) { 77 | throw new BaseServerException("TOO_MANY_RESULTS_EXCEPTION", size); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/AuthUtil.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import org.springframework.lang.NonNull; 4 | import org.springframework.lang.Nullable; 5 | import org.springframework.web.context.request.RequestContextHolder; 6 | import org.springframework.web.context.request.ServletRequestAttributes; 7 | import top.rizon.springbestpractice.common.exception.AuthFailedException; 8 | import top.rizon.springbestpractice.common.model.dto.AuthUser; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | 12 | /** 13 | * @author Rizon 14 | * @date 2019/12/3 15 | */ 16 | public class AuthUtil { 17 | private static final String USER_KEY = "AUTH_USER_BEAN"; 18 | 19 | public static void setAuthUser(HttpServletRequest request, AuthUser authUser) { 20 | request.setAttribute(USER_KEY, authUser); 21 | } 22 | 23 | /** 24 | * @throws AuthFailedException 认证失败会报错 25 | */ 26 | @NonNull 27 | public static AuthUser authUserOrFailed() { 28 | Object authUser = getRequest().getAttribute(USER_KEY); 29 | if (!(authUser instanceof AuthUser)) { 30 | throw new AuthFailedException("AUTH_FAILED"); 31 | } 32 | return (AuthUser) authUser; 33 | } 34 | 35 | @Nullable 36 | public static AuthUser getAuthUser(HttpServletRequest request) { 37 | Object authUser = request.getAttribute(USER_KEY); 38 | if (authUser instanceof AuthUser) { 39 | return (AuthUser) authUser; 40 | } 41 | return null; 42 | } 43 | 44 | private static HttpServletRequest getRequest() { 45 | return getRequestAttributes().getRequest(); 46 | } 47 | 48 | private static ServletRequestAttributes getRequestAttributes() { 49 | ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 50 | if (requestAttributes == null) { 51 | throw new AuthFailedException("requestAttributes is null,not http request"); 52 | } 53 | return requestAttributes; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/DataDate.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import com.alibaba.fastjson.annotation.JSONCreator; 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonValue; 6 | import lombok.EqualsAndHashCode; 7 | import org.jetbrains.annotations.Contract; 8 | import org.joda.time.DateTime; 9 | import top.rizon.springbestpractice.common.exception.BaseServerException; 10 | 11 | import java.util.Date; 12 | 13 | /** 14 | * @author Rizon 15 | * @date 2019-08-19 16 | */ 17 | @EqualsAndHashCode(callSuper = true) 18 | public class DataDate extends Date { 19 | private static String DATA_DATE_FORMAT = DateTimeUtils.YYYYMMDD; 20 | private static final String MOCK_CUR_TIME = System.getProperty("MOCK_CUR_TIME"); 21 | /** 22 | * 保存一个时间数据用于快速检查是否为today对象 23 | */ 24 | private Long fastTodayTime; 25 | 26 | public DataDate() { 27 | this(new DateTime().toString(DATA_DATE_FORMAT)); 28 | this.fastTodayTime = this.getTime(); 29 | } 30 | 31 | @Contract(pure = true) 32 | public DataDate(Date date) { 33 | this(date.getTime()); 34 | } 35 | 36 | public DataDate(long date) { 37 | this(new DateTime(date).toString(DATA_DATE_FORMAT)); 38 | } 39 | 40 | @JsonCreator 41 | @JSONCreator 42 | public DataDate(String dateStr) { 43 | super(DateTimeUtils.parse(dateStr, DATA_DATE_FORMAT).getTime()); 44 | } 45 | 46 | public static DataDate of(String dateStr) { 47 | try { 48 | return new DataDate(dateStr); 49 | } catch (Exception ex) { 50 | throw new BaseServerException("日期格式错误:%s,期望的格式为:%s", dateStr, DATA_DATE_FORMAT); 51 | } 52 | 53 | } 54 | 55 | public static DataDate of(Date date) { 56 | return new DataDate(date); 57 | } 58 | 59 | public static DataDate of(DateTime datetime) { 60 | return new DataDate(datetime.getMillis()); 61 | } 62 | 63 | 64 | public boolean isToday() { 65 | return DataDate.isToday(this); 66 | } 67 | 68 | public static boolean isToday(Date date) { 69 | //快速日期检查 70 | if (date instanceof DataDate && 71 | ((DataDate) date).fastTodayTime != null && 72 | ((DataDate) date).fastTodayTime == date.getTime()) { 73 | return true; 74 | } 75 | 76 | DateTime now = new DateTime(DataDate.now().getTime()); 77 | long beginMills = now.millisOfDay().withMinimumValue().getMillis(); 78 | long endMills = now.millisOfDay().withMaximumValue().getMillis(); 79 | long thisMills = date.getTime(); 80 | return thisMills >= beginMills && thisMills <= endMills; 81 | } 82 | 83 | public static DataDate now() { 84 | if (MOCK_CUR_TIME != null) { 85 | DataDate date = new DataDate(MOCK_CUR_TIME); 86 | date.fastTodayTime = date.getTime(); 87 | return date; 88 | } 89 | return new DataDate(); 90 | } 91 | 92 | @JsonValue 93 | @Contract(pure = true) 94 | public String toQuery() { 95 | return DateTimeUtils.format(this, DATA_DATE_FORMAT); 96 | } 97 | 98 | @Contract(pure = true) 99 | public String toEsId() { 100 | return DateTimeUtils.format(this, DateTimeUtils.YYYYMMDD_NOLINE); 101 | } 102 | 103 | @Contract(pure = true) 104 | public Date toDate() { 105 | return new Date(this.getTime()); 106 | } 107 | 108 | public String toString(String pattern) { 109 | return DateTimeUtils.format(this, pattern); 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return toQuery(); 115 | } 116 | 117 | public static class Builder { 118 | public static DataDate newInstance(String pattern) { 119 | return new DataDate() { 120 | { 121 | DATA_DATE_FORMAT = pattern; 122 | } 123 | }; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/DateTimeUtils.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import lombok.NonNull; 4 | import org.joda.time.DateTime; 5 | import top.rizon.springbestpractice.common.exception.BaseServerException; 6 | 7 | import java.text.ParseException; 8 | import java.text.SimpleDateFormat; 9 | import java.util.Date; 10 | 11 | /** 12 | * @author Rizon 13 | * @date 2019/12/26 14 | */ 15 | public class DateTimeUtils { 16 | public static final String YYYYMMDDHHMMSS = "yyyy-MM-dd HH:mm:ss"; 17 | public static final String YYYYMMDDHHMM = "yyyy-MM-dd HH:mm"; 18 | public static final String YYYYMMDDHHMM_ZH = "yyyy年MM月dd HH点mm分"; 19 | public static final String YYYYMMDD_ZH = "yyyy年MM月dd日"; 20 | public static final String YYYYMMDDHHMMSS_ZH = "yyyy年MM月dd日 HH点mm分ss秒"; 21 | public static final String YYYYMMDD = "yyyy-MM-dd"; 22 | public static final String YYYYMM = "yyyy-MM"; 23 | public static final String YYYYMM_NOLINE = "yyyyMM"; 24 | public static final String YYYYMM_ZH = "yyyy年MM月"; 25 | public static final String YYYY = "yyyy"; 26 | public static final String HHMMSS = "HH:mm:ss"; 27 | public static final String MMDDHHMMSS = "MM-dd HH:mm:ss"; 28 | public static final String DDHHMMSS = "dd HH:mm:ss"; 29 | 30 | /*** 31 | * 没有中划线 32 | */ 33 | public static final String YYYYMMDD_NOLINE = "yyyyMMdd"; 34 | public static final String YYYYMMDDHH_NOLINE = "yyyyMMddHH"; 35 | public static final String YYYYMMDDHHMM_NOLINE = "yyyyMMddHHmm"; 36 | 37 | 38 | @NonNull 39 | public static Date parse(@NonNull String timeStr, @NonNull String pattern) { 40 | try { 41 | return new SimpleDateFormat(pattern).parse(timeStr); 42 | } catch (ParseException ex) { 43 | throw new BaseServerException(ex, 44 | "could not parse date: %s,pattern:%s", timeStr, pattern); 45 | } 46 | } 47 | 48 | @NonNull 49 | public static String format(@NonNull Date date, @NonNull String pattern) { 50 | return new SimpleDateFormat(pattern).format(date); 51 | } 52 | 53 | /** 54 | * 获取给定时间所在月份的天数 55 | */ 56 | public static int getMaxDayOfMonth(Date date) { 57 | return new DateTime(date).dayOfMonth().withMaximumValue().getDayOfMonth(); 58 | } 59 | 60 | /** 61 | * 当月最后一天的最大时间 62 | */ 63 | public static Date getLastDayOfMonth(Date date) { 64 | return new DateTime(date).dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate(); 65 | } 66 | 67 | /** 68 | * 当月第一天的最小时间最小时间 69 | */ 70 | public static Date getFirstDayOfMonth(DateTime date) { 71 | return date.dayOfMonth().withMinimumValue().withTimeAtStartOfDay().toDate(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/ExceptionUtils.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import jodd.exception.ExceptionUtil; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019-10-21 9 | */ 10 | public class ExceptionUtils { 11 | public static String rootCauseToString(Exception ex) { 12 | return ExceptionUtil.exceptionStackTraceToString(ExceptionUtil.getRootCause(ex)); 13 | } 14 | 15 | public static String rootCauseToString(Exception ex, int limit) { 16 | return StringUtils.substring(rootCauseToString(ex), 0, limit); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/JacksonUtil.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.SerializationFeature; 8 | import top.rizon.springbestpractice.common.exception.BaseServerException; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * @author Rizon 14 | * @date 2019/12/13 15 | */ 16 | public class JacksonUtil { 17 | private static final ObjectMapper OBJECT_MAPPER; 18 | 19 | static { 20 | OBJECT_MAPPER = new ObjectMapper(); 21 | OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 22 | OBJECT_MAPPER.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); 23 | OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 24 | OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 25 | OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); 26 | } 27 | 28 | public static T parseObject(String json, Class clazz) { 29 | try { 30 | return OBJECT_MAPPER.readValue(json, clazz); 31 | } catch (IOException e) { 32 | throw new BaseServerException("json parse object failed:" + json, e); 33 | } 34 | } 35 | 36 | public static String toJsonString(Object object) { 37 | try { 38 | return OBJECT_MAPPER.writeValueAsString(object); 39 | } catch (JsonProcessingException e) { 40 | throw new BaseServerException("object parse json failed", e); 41 | } 42 | } 43 | 44 | 45 | public static T cloneObj(Object source, Class clazz) { 46 | return parseObject(toJsonString(source), clazz); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/MapUtils.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import lombok.NonNull; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * top.rizon.springbestpractice.common.utils.MapUtilsTest 14 | * 15 | * @author Rizon 16 | * @date 2019-06-22 17 | */ 18 | public class MapUtils { 19 | 20 | public static List> flatMaps(@NonNull List> maps, String joinKey) { 21 | return maps.stream().map(map -> flatMap(map, joinKey)).collect(Collectors.toList()); 22 | } 23 | 24 | /** 25 | * map扁平化处理 26 | * 27 | * @param map 28 | * @param joinKey 拼接key的连接字符 29 | * @return 30 | */ 31 | public static Map flatMap(@NonNull Map map, String joinKey) { 32 | return recursiveFlat(map, joinKey, new ArrayList<>()); 33 | } 34 | 35 | public static List> formatMaps(@NonNull List> maps, String joinKey) { 36 | return maps.stream().map(map -> formatMap(map, joinKey)).collect(Collectors.toList()); 37 | } 38 | 39 | /** 40 | * 将扁平化的map还原 41 | * 42 | * @param map 43 | * @param joinKey 扁平化时key的连接字符,用于分割key 44 | * @return 45 | */ 46 | public static Map formatMap(@NonNull Map map, String joinKey) { 47 | Map result = new HashMap<>(); 48 | for (Map.Entry entry : map.entrySet()) { 49 | String key = entry.getKey(); 50 | Object value = entry.getValue(); 51 | String[] keyPath = key.split(joinKey); 52 | putIntoMapByPath(result, value, keyPath); 53 | } 54 | return result; 55 | } 56 | 57 | 58 | private static Map recursiveFlat(Map map, String joinKey, List path) { 59 | Map result = new HashMap<>(); 60 | 61 | for (Map.Entry entry : map.entrySet()) { 62 | String key = entry.getKey(); 63 | Object value = entry.getValue(); 64 | 65 | if (value instanceof Map) { 66 | ArrayList newPth = new ArrayList<>(path); 67 | newPth.add(key); 68 | Map subMap = recursiveFlat((Map) value, joinKey, newPth); 69 | result.putAll(subMap); 70 | } else { 71 | String parentPath = StringUtils.join(path, joinKey); 72 | result.put(StringUtils.isEmpty(parentPath) ? key : parentPath.concat(joinKey).concat(key), value); 73 | } 74 | 75 | } 76 | return result; 77 | } 78 | 79 | private static void putIntoMapByPath(Map result, Object value, String[] keyPath) { 80 | Map map = result; 81 | for (int i = 0; i < keyPath.length - 1; i++) { 82 | String key = keyPath[i]; 83 | if (result.containsKey(key)) { 84 | map = (Map) map.get(key); 85 | } else { 86 | HashMap newMap = new HashMap<>(); 87 | map.put(key, newMap); 88 | map = newMap; 89 | } 90 | } 91 | String lastKey = keyPath[keyPath.length - 1]; 92 | map.put(lastKey, value); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/ObjUtil.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import org.springframework.lang.NonNull; 4 | import org.springframework.lang.Nullable; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/25 9 | */ 10 | public class ObjUtil { 11 | 12 | public static T defaultValue(@Nullable T value, @NonNull T defaultValue) { 13 | return value == null ? defaultValue : value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/ResponseUtil.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | /** 6 | * 在一些场景中我们屏蔽掉了一些服务端异常情况让接口可以正常返回数据, 7 | * 但是又希望可以从接口吐出这些异常信息以供调试使用, 8 | * 此时可以借助该工具实现 9 | * 10 | * @author Rizon 11 | * @date 2019-10-19 12 | */ 13 | public class ResponseUtil { 14 | private static final ThreadLocal ERROR_MSG = new ThreadLocal<>(); 15 | 16 | public static void setThreadErrorMsg(String message) { 17 | ERROR_MSG.remove(); 18 | ERROR_MSG.set(message); 19 | } 20 | 21 | public static String getErrorMsg() { 22 | return ERROR_MSG.get(); 23 | } 24 | 25 | public static void appendThreadErrorMsg(String message) { 26 | ERROR_MSG.set(message + ";" + StringUtils.defaultString(ERROR_MSG.get())); 27 | } 28 | 29 | public static String popErrorMsg() { 30 | String msg = ERROR_MSG.get(); 31 | ERROR_MSG.remove(); 32 | return msg; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/StreamUtil.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import lombok.NonNull; 4 | 5 | import java.util.Collection; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.function.Function; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * java8 stream 的一些用法的封装 13 | * 14 | * @author Rizon 15 | * @date 2019/12/30 16 | */ 17 | public class StreamUtil { 18 | 19 | public static Map list2Map(@NonNull Collection list, @NonNull Function keyFunc) { 20 | return list.stream().collect(Collectors.toMap(keyFunc, Function.identity(), 21 | (u, v) -> { 22 | throw new IllegalStateException(String.format("Multiple entries with same key,%s=%s,%s=%s", 23 | keyFunc.apply(u), u, 24 | keyFunc.apply(v), v)); 25 | }, 26 | HashMap::new)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/TraceUtils.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.lang3.RandomStringUtils; 5 | import org.slf4j.MDC; 6 | 7 | 8 | /** 9 | * 系统运行时打印方便调试与追踪信息的工具类. 10 | * 使用MDC存储traceID, 一次trace中所有日志都自动带有该ID, 11 | * 可以方便的用grep命令在日志文件中提取该trace的所有日志. 12 | * 13 | * @author Rizon 14 | * @date 2019/10/24 15 | */ 16 | @Slf4j 17 | public class TraceUtils { 18 | private static final String TRACE_KEY = "TRACE_KEY"; 19 | private static final int TRACE_ID_LENGTH = 8; 20 | 21 | 22 | /** 23 | * 开始Trace, 将指定key的value放入MDC. 24 | */ 25 | public static void beginTaskTrace(String requestId) { 26 | try { 27 | MDC.put(TRACE_KEY, requestId); 28 | } catch (Exception ex) { 29 | log.error("trace begin error", ex); 30 | } 31 | } 32 | 33 | /** 34 | * 结束Trace. 35 | * 清除traceId. 36 | */ 37 | public static void endTaskTrace() { 38 | try { 39 | MDC.remove(TRACE_KEY); 40 | } catch (Exception ex) { 41 | log.error("trace end error", ex); 42 | } 43 | } 44 | 45 | public static String getTraceId() { 46 | try { 47 | return MDC.get(TRACE_KEY); 48 | } catch (Exception ex) { 49 | log.error("get trace id error", ex); 50 | return null; 51 | } 52 | } 53 | 54 | public static String randomTraceId() { 55 | return RandomStringUtils.randomAlphanumeric(TRACE_ID_LENGTH); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/http/BaseAuthHeaderHttpRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils.http; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.springframework.http.HttpRequest; 6 | import org.springframework.http.client.ClientHttpRequestExecution; 7 | import org.springframework.http.client.ClientHttpRequestInterceptor; 8 | import org.springframework.http.client.ClientHttpResponse; 9 | import org.springframework.util.AntPathMatcher; 10 | 11 | import java.io.IOException; 12 | import java.util.List; 13 | 14 | /** 15 | * RestTemplate请求认证header注入 16 | * 17 | * @author Rizon 18 | * @date 2019/12/3 19 | */ 20 | public abstract class BaseAuthHeaderHttpRequestInterceptor implements ClientHttpRequestInterceptor { 21 | private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); 22 | 23 | @NotNull 24 | @Override 25 | public ClientHttpResponse intercept(HttpRequest request, @NotNull byte[] body, @NotNull ClientHttpRequestExecution execution) 26 | throws IOException { 27 | String url = request.getURI().toString(); 28 | List includePathPatterns = includePathPatterns(); 29 | //如果需要认证 30 | if (includePathPatterns == null || 31 | includePathPatterns.stream().anyMatch(pattern -> PATH_MATCHER.match(pattern, url))) { 32 | process(request); 33 | } 34 | // 保证请求继续被执行 35 | return execution.execute(request, body); 36 | } 37 | 38 | /** 39 | * 拦截器的request处理方法 40 | * @param request HttpRequest 41 | */ 42 | public abstract void process(HttpRequest request); 43 | 44 | /** 45 | * 需要认证请求的前缀 46 | * 47 | * @return 如果返回null则全部生效 48 | */ 49 | @Nullable 50 | public abstract List includePathPatterns(); 51 | } 52 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/http/RestTemplateAuthConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils.http; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.collections4.ListUtils; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.springframework.beans.BeansException; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.http.client.ClientHttpRequestInterceptor; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * @author Rizon 19 | * @date 2019/12/3 20 | */ 21 | @Configuration 22 | @Slf4j 23 | public class RestTemplateAuthConfig implements ApplicationContextAware, InitializingBean { 24 | private ApplicationContext applicationContext; 25 | private final RestTemplate restTemplate; 26 | 27 | public RestTemplateAuthConfig(RestTemplate restTemplate) { 28 | this.restTemplate = restTemplate; 29 | } 30 | 31 | @Override 32 | public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException { 33 | this.applicationContext = applicationContext; 34 | } 35 | 36 | @Override 37 | public void afterPropertiesSet() throws Exception { 38 | Map authInterceptors = applicationContext.getBeansOfType(BaseAuthHeaderHttpRequestInterceptor.class); 39 | List interceptors = 40 | ListUtils.emptyIfNull(restTemplate.getInterceptors()); 41 | for (Map.Entry entry : authInterceptors.entrySet()) { 42 | log.info("add restTemplate auth interceptor:{}", entry.getKey()); 43 | interceptors.add(entry.getValue()); 44 | } 45 | SimpleRestTemplateUtils.getRestTemplate().setInterceptors(interceptors); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/src/main/java/top/rizon/springbestpractice/common/utils/http/SimpleRestTemplateUtils.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils.http; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.jetbrains.annotations.Contract; 7 | import org.jetbrains.annotations.Nullable; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.core.ParameterizedTypeReference; 10 | import org.springframework.http.*; 11 | import org.springframework.http.client.ClientHttpRequestFactory; 12 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.LinkedMultiValueMap; 15 | import org.springframework.web.client.HttpClientErrorException; 16 | import org.springframework.web.client.HttpServerErrorException; 17 | import org.springframework.web.client.RestTemplate; 18 | import org.springframework.web.util.UriComponentsBuilder; 19 | import top.rizon.springbestpractice.common.exception.BaseServerException; 20 | import top.rizon.springbestpractice.common.handler.RestClientErrorExceptionHandler; 21 | import top.rizon.springbestpractice.common.model.response.Response; 22 | import top.rizon.springbestpractice.common.utils.ObjUtil; 23 | 24 | import java.net.URI; 25 | import java.util.Map; 26 | 27 | /** 28 | * @author Rizon 29 | * @date 2019/12/2 30 | */ 31 | @Component 32 | @Slf4j 33 | public class SimpleRestTemplateUtils { 34 | private static final RestClientErrorExceptionHandler DEFAULT_ERROR_HANDLER = new RestClientErrorExceptionHandler(); 35 | @Setter 36 | private Integer connectTimeout = 3000; 37 | @Setter 38 | private Integer readTimeout = 6000; 39 | @Getter 40 | private static RestTemplate restTemplate; 41 | 42 | public static R doGetRes(String url, Map queryMap, HttpHeaders httpHeaders, ParameterizedTypeReference responseType) { 43 | return doGetRes(url, queryMap, httpHeaders, responseType, null); 44 | } 45 | 46 | public static R doGetRes(String url, Map queryMap, HttpHeaders httpHeaders, ParameterizedTypeReference responseType, RestClientErrorExceptionHandler exceptionHandler) { 47 | LinkedMultiValueMap params = new LinkedMultiValueMap<>(); 48 | if (queryMap != null) { 49 | params.setAll(queryMap); 50 | } 51 | URI uri = UriComponentsBuilder.fromHttpUrl(url) 52 | .queryParams(params) 53 | .encode().build().toUri(); 54 | log.info("get uri:{}", uri); 55 | 56 | ResponseEntity exchange; 57 | try { 58 | exchange = restTemplate.exchange(uri, HttpMethod.GET, 59 | new HttpEntity<>(httpHeaders), responseType); 60 | } catch (HttpClientErrorException | HttpServerErrorException ex) { 61 | if (exceptionHandler == null) { 62 | return DEFAULT_ERROR_HANDLER.doHandler(ex); 63 | } else { 64 | return exceptionHandler.doHandler(ex); 65 | } 66 | } 67 | return exchange.getBody(); 68 | } 69 | 70 | @Nullable 71 | public static R doPost(String url, T queryMap, ParameterizedTypeReference> responseType) { 72 | Response response = doPostRes(url, queryMap, responseType); 73 | responseCheck(response); 74 | return response.getResult(); 75 | } 76 | 77 | @Nullable 78 | public static R doPostRes(String url, T queryMap, ParameterizedTypeReference responseType) { 79 | return doPostRes(url, queryMap, responseType, null); 80 | } 81 | 82 | @Nullable 83 | public static R doPostRes(String url, T queryMap, ParameterizedTypeReference responseType, RestClientErrorExceptionHandler exceptionHandler) { 84 | log.info("post url:{},param:{}", url, queryMap); 85 | HttpHeaders authHeader = new HttpHeaders(); 86 | authHeader.setContentType(MediaType.APPLICATION_JSON); 87 | HttpEntity entity = new HttpEntity<>(queryMap, authHeader); 88 | ResponseEntity exchange; 89 | try { 90 | exchange = restTemplate.exchange(url, HttpMethod.POST, entity, responseType); 91 | } catch (HttpClientErrorException | HttpServerErrorException ex) { 92 | if (exceptionHandler == null) { 93 | return DEFAULT_ERROR_HANDLER.doHandler(ex); 94 | } else { 95 | return exceptionHandler.doHandler(ex); 96 | } 97 | } 98 | return exchange.getBody(); 99 | } 100 | 101 | @Bean 102 | public RestTemplate restTemplate(ClientHttpRequestFactory factory) { 103 | SimpleRestTemplateUtils.restTemplate = new RestTemplate(factory); 104 | return SimpleRestTemplateUtils.restTemplate; 105 | } 106 | 107 | @Bean 108 | public ClientHttpRequestFactory simpleClientHttpRequestFactory() { 109 | SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); 110 | factory.setConnectTimeout(ObjUtil.defaultValue(connectTimeout, 3000)); 111 | factory.setReadTimeout(ObjUtil.defaultValue(readTimeout, 5000)); 112 | return factory; 113 | } 114 | 115 | @Contract("null -> fail") 116 | public static void responseCheck(@Nullable Response response) { 117 | if (response == null || response.getStatus() != Response.CODE_SUCCESS) { 118 | throw new BaseServerException("http请求返回数据错误:" + response); 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /common/src/test/java/top/rizon/springbestpractice/AppTest.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest 11 | { 12 | /** 13 | * Rigorous Test :-) 14 | */ 15 | @Test 16 | public void shouldAnswerWithTrue() 17 | { 18 | assertTrue( true ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/test/java/top/rizon/springbestpractice/common/utils/MapUtilsTest.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.alibaba.fastjson.TypeReference; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * @author Rizon 12 | * @date 2019/12/26 13 | */ 14 | public class MapUtilsTest { 15 | 16 | @Test 17 | public void flatMap() { 18 | String json = "{" + 19 | " \"1\":\"value 1\"," + 20 | " \"2\":{" + 21 | " \"2.1\":{" + 22 | " \"2.1.1\":\"value 2.1.1\"" + 23 | " }" + 24 | " }" + 25 | "}"; 26 | Map map = JSONObject.parseObject(json, new TypeReference>() { 27 | }); 28 | Map flatMap = MapUtils.flatMap(map, ":"); 29 | Assert.assertEquals("{\"1\":\"value 1\",\"2:2.1:2.1.1\":\"value 2.1.1\"}", 30 | JSONObject.toJSONString(flatMap)); 31 | } 32 | 33 | @Test 34 | public void formatMap() { 35 | String json = "{\"1\":\"value 1\",\"2:2.1:2.1.1\":\"value 2.1.1\"}"; 36 | Map map = JSONObject.parseObject(json, new TypeReference>() { 37 | }); 38 | Map formatMap = MapUtils.formatMap(map, ":"); 39 | Assert.assertEquals("{\"1\":\"value 1\",\"2\":{\"2.1\":{\"2.1.1\":\"value 2.1.1\"}}}", 40 | JSONObject.toJSONString(formatMap)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /common/src/test/java/top/rizon/springbestpractice/common/utils/StreamUtilTest.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.common.utils; 2 | 3 | import com.google.common.collect.Maps; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.RequiredArgsConstructor; 7 | import org.junit.Test; 8 | 9 | import java.util.*; 10 | import java.util.function.BinaryOperator; 11 | import java.util.stream.Collectors; 12 | 13 | /** 14 | * @author Rizon 15 | * @date 2019/12/30 16 | * @see StreamUtil 17 | */ 18 | public class StreamUtilTest { 19 | 20 | List persons = Arrays.asList( 21 | new Person("zhang", "A1", 12, "M"), 22 | new Person("wang", "B1", 15, "M"), 23 | new Person("li", "A1", 11, "W"), 24 | new Person("wu", "B2", 11, "W")); 25 | 26 | 27 | @Test 28 | public void base() { 29 | System.out.println("map filter orElse DEMO: " + 30 | persons.stream() 31 | .map(Person::getName) 32 | .filter("notExist"::equals) 33 | .findAny() 34 | .orElse("default") 35 | ); 36 | 37 | } 38 | 39 | /** 40 | * 使用stream和google的maps工具类的list转map 41 | */ 42 | @Test 43 | public void list2Map() { 44 | // java8 stream 的list转map 45 | Map byName = StreamUtil.list2Map(persons, Person::getName); 46 | System.out.println("java8 stream: " + byName); 47 | // google maps工具类 48 | byName = Maps.uniqueIndex(persons, Person::getName); 49 | System.out.println("maps util: " + byName); 50 | 51 | System.out.println("====key重复的错误信息提示===="); 52 | //key重复的错误信息提示 53 | try { 54 | StreamUtil.list2Map(persons, Person::getLocation); 55 | } catch (Exception ex) { 56 | ex.printStackTrace(System.out); 57 | } 58 | 59 | try { 60 | Maps.uniqueIndex(persons, Person::getLocation); 61 | } catch (Exception ex) { 62 | ex.printStackTrace(System.out); 63 | } 64 | } 65 | 66 | /** 67 | * {@code reduce(accumulator) } :参数是一个执行双目运算的 Functional Interface , 68 | * 假如这个参数表示的操作为op,stream中的元素为x, y, z, …,则 reduce() 执行的就是{@code x op y op z ... } , 69 | * 所以要求op这个操作具有结合性(associative),即满足:{@code (x op y) op z = x op (y op z) }, 70 | * 满足这个要求的操作主要有:求和、求积、求最大值、求最小值、字符串连接、集合并集和交集等。 71 | * 另外,该函数的返回值是Optional的: 72 | *

73 | *

{@code
 74 |      * Optional sum1 = numStream.reduce((x, y) -> x + y);
 75 |      * }
76 | *

77 | * {@code reduce(identity, accumulator) } :可以认为第一个参数为默认值,但需要满足{@code identity op x = x }, 78 | * 所以对于求和操作, identity 的值为0,对于求积操作, identity 的值为1。返回值类型是stream元素的类型: 79 | *

80 | *

{@code
 81 |      * Integer sum2 = numStream.reduce(0, Integer::sum);
 82 |      * }
83 | *

84 | * reduce 如果不加参数`identity`则返回的是optional类型的,reduce在进行双目运算时, 85 | * 其中一个场景是与`identity`做比较操作,因此我们应该满足 {@code identity op x = x } 86 | *

87 | * 参考:https://www.cnblogs.com/zxf330301/p/6586750.html 88 | */ 89 | @Test 90 | public void reduce() { 91 | Person identity = new Person(null, null, 0, null); 92 | List maxAge = persons.stream().collect( 93 | Collectors.collectingAndThen( 94 | //按性别分组 95 | Collectors.groupingBy(Person::getSex, 96 | //每组取年龄最大的 97 | Collectors.reducing(identity, BinaryOperator.maxBy(Comparator.comparing(Person::getAge)))), 98 | //合并各组的值 99 | p -> new ArrayList<>(p.values())) 100 | ); 101 | System.out.println(maxAge); 102 | } 103 | 104 | /** 105 | * stream并发 106 | * parallelStream默认使用了fork-join框架,其默认线程数是CPU核心数。 107 | */ 108 | @Test 109 | public void parallel() { 110 | //非并发遍历 111 | System.out.println("===非并发遍历==="); 112 | persons.forEach(System.out::println); 113 | //并发遍历 114 | System.out.println("===并发遍历1==="); 115 | persons.parallelStream().forEach(System.out::println); 116 | System.out.println("===并发遍历2==="); 117 | persons.parallelStream().forEach(System.out::println); 118 | 119 | } 120 | 121 | @RequiredArgsConstructor 122 | @AllArgsConstructor 123 | @Data 124 | public static class Person { 125 | private final String name; 126 | private String location; 127 | private Integer age; 128 | private String sex; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /dao/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-best-practice 7 | top.rizon.springbestpractice 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | dao 12 | 13 | 14 | 15 | top.rizon.springbestpractice 16 | common 17 | ${project.version} 18 | 19 | 20 | 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-compiler-plugin 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/config/MybatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.config; 2 | 3 | import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; 4 | import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; 5 | import org.mybatis.spring.annotation.MapperScan; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import top.rizon.springbestpractice.dao.utils.dynamictblname.DynamicTableNameUtils; 10 | 11 | /** 12 | * @author rizon 13 | * @since 2018-08-10 14 | */ 15 | @Configuration 16 | @MapperScan("top.rizon.springbestpractice.dao.mapper") 17 | public class MybatisPlusConfig { 18 | /** 19 | * 分页插件 20 | */ 21 | 22 | @Bean 23 | public PaginationInterceptor paginationInterceptor() { 24 | PaginationInterceptor interceptor = new PaginationInterceptor(); 25 | DynamicTableNameUtils.registerDynamicTableName(interceptor); 26 | return interceptor; 27 | } 28 | 29 | /** 30 | * SQL执行效率插件 31 | * 只在 dev test 环境开启 32 | */ 33 | @Bean 34 | @Profile({"dev", "test"}) 35 | public PerformanceInterceptor performanceInterceptor() { 36 | return new PerformanceInterceptor(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/helper/UserHelper.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.helper; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import top.rizon.springbestpractice.dao.po.User; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/25 9 | */ 10 | public interface UserHelper extends IService { 11 | } 12 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/helper/impl/UserHelperImpl.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.helper.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import org.springframework.stereotype.Service; 5 | import top.rizon.springbestpractice.dao.helper.UserHelper; 6 | import top.rizon.springbestpractice.dao.mapper.UserMapper; 7 | import top.rizon.springbestpractice.dao.po.User; 8 | 9 | /** 10 | * @author Rizon 11 | * @date 2019/12/25 12 | */ 13 | @Service 14 | public class UserHelperImpl extends ServiceImpl implements UserHelper { 15 | } 16 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/mapper/HistoryMapper.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import top.rizon.springbestpractice.dao.po.HistoryPo; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/27 9 | */ 10 | public interface HistoryMapper extends BaseMapper { 11 | } 12 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import top.rizon.springbestpractice.dao.po.User; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/25 9 | */ 10 | public interface UserMapper extends BaseMapper { 11 | } 12 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/po/HistoryPo.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.po; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableId; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import lombok.Data; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.io.Serializable; 9 | import java.time.LocalDateTime; 10 | 11 | /** 12 | * @author Rizon 13 | * @date 2019/12/27 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | @TableName("HistoryFakeTableName") 18 | public class HistoryPo implements Serializable { 19 | 20 | private static final long serialVersionUID = 1L; 21 | 22 | @TableId 23 | private Long id; 24 | private String data; 25 | private LocalDateTime createTime; 26 | } 27 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/po/User.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.po; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @author Rizon 13 | * @date 2019/12/25 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | @TableName("user") 18 | public class User implements Serializable { 19 | private static final long serialVersionUID = 1L; 20 | 21 | @TableId(value = "id", type = IdType.AUTO) 22 | private Long id; 23 | private String name; 24 | private Integer age; 25 | private String email; 26 | private String token; 27 | } 28 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/utils/dynamictblname/DynamicTableNameParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2020, baomidou (jobob@qq.com). 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package top.rizon.springbestpractice.dao.utils.dynamictblname; 17 | 18 | import com.baomidou.mybatisplus.core.parser.ISqlParser; 19 | import com.baomidou.mybatisplus.core.parser.SqlInfo; 20 | import com.baomidou.mybatisplus.core.toolkit.Assert; 21 | import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; 22 | import lombok.Data; 23 | import lombok.experimental.Accessors; 24 | import org.apache.ibatis.reflection.MetaObject; 25 | 26 | import java.util.Collection; 27 | import java.util.Map; 28 | 29 | /** 30 | * 动态表名 SQL 解析器 31 | * 32 | * @author jobob 33 | * @since 2019-04-23 34 | */ 35 | @Data 36 | @Accessors(chain = true) 37 | public class DynamicTableNameParser implements ISqlParser { 38 | 39 | private Map tableNameHandlerMap; 40 | 41 | @Override 42 | public SqlInfo parser(MetaObject metaObject, String sql) { 43 | Assert.isFalse(CollectionUtils.isEmpty(tableNameHandlerMap), "tableNameHandlerMap is empty."); 44 | if (allowProcess(metaObject)) { 45 | Collection tables = new TableNameParser(sql).tables(); 46 | if (CollectionUtils.isNotEmpty(tables)) { 47 | boolean sqlParsed = false; 48 | String parsedSql = sql; 49 | for (final String table : tables) { 50 | ITableNameHandler tableNameHandler = tableNameHandlerMap.get(table); 51 | if (null != tableNameHandler) { 52 | parsedSql = tableNameHandler.process(metaObject, parsedSql, table); 53 | sqlParsed = true; 54 | } 55 | } 56 | if (sqlParsed) { 57 | return SqlInfo.newInstance().setSql(parsedSql); 58 | } 59 | } 60 | } 61 | return null; 62 | } 63 | 64 | 65 | /** 66 | * 判断是否允许执行 67 | *

例如:逻辑删除只解析 delete , update 操作

68 | * 69 | * @param metaObject 元对象 70 | * @return true 71 | */ 72 | public boolean allowProcess(MetaObject metaObject) { 73 | return true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/utils/dynamictblname/DynamicTableNameUtils.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.dao.utils.dynamictblname; 2 | 3 | import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang3.StringUtils; 8 | import top.rizon.springbestpractice.common.exception.BaseServerException; 9 | import top.rizon.springbestpractice.common.utils.DataDate; 10 | import top.rizon.springbestpractice.common.utils.DateTimeUtils; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | /** 17 | * @author Rizon 18 | * @date 2019-07-10 19 | */ 20 | @Slf4j 21 | public class DynamicTableNameUtils { 22 | private static final String HISTORY_FAKE_TABLE_NAME = "HistoryFakeTableName"; 23 | private static final ThreadLocal LOCAL_TABLE_NAME = new ThreadLocal<>(); 24 | 25 | /** 26 | * 注入到mybatis-plus拦截器中 27 | * 28 | * @param paginationInterceptor 29 | */ 30 | public static void registerDynamicTableName(PaginationInterceptor paginationInterceptor) { 31 | Map tableNameHandlerMap = new HashMap<>(1); 32 | tableNameHandlerMap.put(HISTORY_FAKE_TABLE_NAME, (metaObject, sql, tableName) -> { 33 | DynamicTableNameParam nameParam = LOCAL_TABLE_NAME.get(); 34 | 35 | if (nameParam != null 36 | && tableName.equals(nameParam.getFakeTblName()) 37 | && StringUtils.isNotBlank(nameParam.getRealTblName())) { 38 | return nameParam.getRealTblName(); 39 | } 40 | throw new BaseServerException("动态表名替换错误,没有发现替换值:" + tableName); 41 | }); 42 | DynamicTableNameParser tableNameParser = new DynamicTableNameParser(); 43 | tableNameParser.setTableNameHandlerMap(tableNameHandlerMap); 44 | //register 45 | paginationInterceptor.setSqlParserList(Collections.singletonList(tableNameParser)); 46 | } 47 | 48 | public static void startTableName(String fakeTableName, String tableName) { 49 | endTableName(); 50 | LOCAL_TABLE_NAME.set(new DynamicTableNameParam(fakeTableName, tableName)); 51 | log.info("start dynamic table query:{}", tableName); 52 | } 53 | 54 | public static void startHistoryTableName(DataDate date) { 55 | startTableName(HISTORY_FAKE_TABLE_NAME, "history_" + date.toString(DateTimeUtils.YYYYMMDD_NOLINE)); 56 | } 57 | 58 | public static void endTableName() { 59 | LOCAL_TABLE_NAME.remove(); 60 | } 61 | 62 | @AllArgsConstructor 63 | @Getter 64 | private static class DynamicTableNameParam { 65 | private String fakeTblName; 66 | private String realTblName; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/utils/dynamictblname/ITableNameHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2020, baomidou (jobob@qq.com). 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package top.rizon.springbestpractice.dao.utils.dynamictblname; 17 | 18 | import org.apache.ibatis.reflection.MetaObject; 19 | 20 | /** 21 | * 动态表名处理器 22 | * 23 | * @author jobob 24 | * @since 2019-04-23 25 | */ 26 | public interface ITableNameHandler { 27 | 28 | /** 29 | * 表名 SQL 处理 30 | * 31 | * @param metaObject 元对象 32 | * @param sql 当前执行 SQL 33 | * @param tableName 表名 34 | * @return 35 | */ 36 | default String process(MetaObject metaObject, String sql, String tableName) { 37 | String dynamicTableName = dynamicTableName(metaObject, sql, tableName); 38 | if (null != dynamicTableName && !dynamicTableName.equalsIgnoreCase(tableName)) { 39 | return sql.replaceAll(tableName, dynamicTableName); 40 | } 41 | return sql; 42 | } 43 | 44 | /** 45 | * 生成动态表名,无改变返回 NULL 46 | * 47 | * @param metaObject 元对象 48 | * @param sql 当前执行 SQL 49 | * @param tableName 表名 50 | * @return String 51 | */ 52 | String dynamicTableName(MetaObject metaObject, String sql, String tableName); 53 | } 54 | -------------------------------------------------------------------------------- /dao/src/main/java/top/rizon/springbestpractice/dao/utils/dynamictblname/TableNameParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2020, Nadeem Mohammad. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package top.rizon.springbestpractice.dao.utils.dynamictblname; 17 | 18 | import java.util.*; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * SQL 表名解析 24 | * https://github.com/mnadeem/sql-table-name-parser 25 | * Ultra light, Ultra fast parser to extract table name out SQLs, supports oracle dialect SQLs as well. 26 | * USE: new TableNameParser(sql).tables() 27 | * 28 | * @author Nadeem Mohammad 29 | * @since 2019-04-22 30 | */ 31 | public final class TableNameParser { 32 | 33 | private static final int NO_INDEX = -1; 34 | private static final String SPACE = " "; 35 | private static final String REGEX_SPACE = "\\s+"; 36 | 37 | private static final String TOKEN_ORACLE_HINT_START = "/*+"; 38 | private static final String TOKEN_ORACLE_HINT_END = "*/"; 39 | private static final String TOKEN_SINGLE_LINE_COMMENT = "--"; 40 | private static String TOKEN_NEWLINE = "\\r\\n|\\r|\\n|\\n\\r"; 41 | private static final String TOKEN_SEMI_COLON = ";"; 42 | private static final String TOKEN_PARAN_START = "("; 43 | private static final String TOKEN_COMMA = ","; 44 | private static final String TOKEN_SET = "set"; 45 | private static final String TOKEN_OF = "of"; 46 | private static final String TOKEN_DUAL = "dual"; 47 | private static final String TOKEN_DELETE = "delete"; 48 | private static final String TOKEN_CREATE = "create"; 49 | private static final String TOKEN_INDEX = "index"; 50 | private static final String TOKEN_ASTERICK = "*"; 51 | 52 | private static final String KEYWORD_JOIN = "join"; 53 | private static final String KEYWORD_INTO = "into"; 54 | private static final String KEYWORD_TABLE = "table"; 55 | private static final String KEYWORD_FROM = "from"; 56 | private static final String KEYWORD_USING = "using"; 57 | private static final String KEYWORD_UPDATE = "update"; 58 | 59 | private static final List concerned = Arrays.asList(KEYWORD_TABLE, KEYWORD_INTO, KEYWORD_JOIN, KEYWORD_USING, KEYWORD_UPDATE); 60 | private static final List ignored = Arrays.asList(TOKEN_PARAN_START, TOKEN_SET, TOKEN_OF, TOKEN_DUAL); 61 | 62 | private Map tables = new HashMap<>(); 63 | 64 | /** 65 | * Extracts table names out of SQL 66 | * @param sql 67 | */ 68 | public TableNameParser(final String sql) { 69 | String noComments = removeComments(sql); 70 | String normalized = normalized(noComments); 71 | String cleansed = clean(normalized); 72 | String[] tokens = cleansed.split(REGEX_SPACE); 73 | int index = 0; 74 | 75 | String firstToken = tokens[index]; 76 | if (isOracleSpecialDelete(firstToken, tokens, index)) { 77 | handleSpecialOracleSpecialDelete(firstToken, tokens, index); 78 | } else if (isCreateIndex(firstToken, tokens, index)) { 79 | handleCreateIndex(firstToken, tokens, index); 80 | } else { 81 | while (moreTokens(tokens, index)) { 82 | String currentToken = tokens[index++]; 83 | 84 | if (isFromToken(currentToken)) { 85 | processFromToken(tokens, index); 86 | } else if (shouldProcess(currentToken)) { 87 | String nextToken = tokens[index++]; 88 | considerInclusion(nextToken); 89 | 90 | if (moreTokens(tokens, index)) { 91 | nextToken = tokens[index++]; 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | private String removeComments(final String sql) { 99 | StringBuilder sb = new StringBuilder(sql); 100 | int nextCommentPosition = sb.indexOf(TOKEN_SINGLE_LINE_COMMENT); 101 | while (nextCommentPosition > -1) { 102 | int end = indexOfRegex(TOKEN_NEWLINE, sb.substring(nextCommentPosition)); 103 | if (end == -1) { 104 | return sb.substring(0, nextCommentPosition); 105 | } else { 106 | sb.replace(nextCommentPosition, end + nextCommentPosition, ""); 107 | } 108 | nextCommentPosition = sb.indexOf(TOKEN_SINGLE_LINE_COMMENT); 109 | } 110 | return sb.toString(); 111 | } 112 | 113 | private int indexOfRegex(String regex, String string) { 114 | Pattern pattern = Pattern.compile(regex); 115 | Matcher matcher = pattern.matcher(string); 116 | return matcher.find() ? matcher.start() : -1; 117 | } 118 | 119 | private String normalized(final String sql) { 120 | String normalized = sql.trim().replaceAll(TOKEN_NEWLINE, SPACE).replaceAll(TOKEN_COMMA, " , ") 121 | .replaceAll("\\(", " ( ").replaceAll("\\)", " ) "); 122 | if (normalized.endsWith(TOKEN_SEMI_COLON)) { 123 | normalized = normalized.substring(0, normalized.length() - 1); 124 | } 125 | return normalized; 126 | } 127 | 128 | private String clean(final String normalized) { 129 | int start = normalized.indexOf(TOKEN_ORACLE_HINT_START); 130 | int end; 131 | if (start != NO_INDEX) { 132 | end = normalized.indexOf(TOKEN_ORACLE_HINT_END); 133 | if (end != NO_INDEX) { 134 | String firstHalf = normalized.substring(0, start); 135 | String secondHalf = normalized.substring(end + 2, normalized.length()); 136 | return firstHalf.trim() + SPACE + secondHalf.trim(); 137 | } 138 | } 139 | return normalized; 140 | } 141 | 142 | private boolean isOracleSpecialDelete(final String currentToken, final String[] tokens, int index) { 143 | index++;// Point to next token 144 | if (TOKEN_DELETE.equals(currentToken)) { 145 | if (moreTokens(tokens, index)) { 146 | String nextToken = tokens[index++]; 147 | if (!KEYWORD_FROM.equals(nextToken) && !TOKEN_ASTERICK.equals(nextToken)) { 148 | return true; 149 | } 150 | } 151 | } 152 | return false; 153 | } 154 | 155 | private void handleSpecialOracleSpecialDelete(final String currentToken, final String[] tokens, int index) { 156 | String tableName = tokens[index + 1]; 157 | considerInclusion(tableName); 158 | } 159 | 160 | private boolean isCreateIndex(String currentToken, String[] tokens, int index) { 161 | index++; // Point to next token 162 | if (TOKEN_CREATE.equals(currentToken.toLowerCase()) && hasIthToken(tokens, index, 3)) { 163 | String nextToken = tokens[index++]; 164 | if (TOKEN_INDEX.equals(nextToken.toLowerCase())) { 165 | return true; 166 | } 167 | 168 | } 169 | return false; 170 | } 171 | 172 | private void handleCreateIndex(String currentToken, String[] tokens, int index) { 173 | String tableName = tokens[index + 4]; 174 | considerInclusion(tableName); 175 | } 176 | 177 | private boolean hasIthToken(String[] tokens, int currentIndex, int tokenNumber) { 178 | if (moreTokens(tokens, currentIndex) && tokens.length > currentIndex + tokenNumber) { 179 | return true; 180 | } 181 | return false; 182 | } 183 | 184 | private boolean shouldProcess(final String currentToken) { 185 | return concerned.contains(currentToken.toLowerCase()); 186 | } 187 | 188 | private boolean isFromToken(final String currentToken) { 189 | return KEYWORD_FROM.equals(currentToken.toLowerCase()); 190 | } 191 | 192 | private void processFromToken(final String[] tokens, int index) { 193 | String currentToken = tokens[index++]; 194 | considerInclusion(currentToken); 195 | 196 | String nextToken = null; 197 | if (moreTokens(tokens, index)) { 198 | nextToken = tokens[index++]; 199 | } 200 | 201 | if (shouldProcessMultipleTables(nextToken)) { 202 | processNonAliasedMultiTables(tokens, index, nextToken); 203 | } else { 204 | processAliasedMultiTables(tokens, index, currentToken); 205 | } 206 | } 207 | 208 | private void processNonAliasedMultiTables(final String[] tokens, int index, String nextToken) { 209 | while (nextToken.equals(TOKEN_COMMA)) { 210 | String currentToken = tokens[index++]; 211 | considerInclusion(currentToken); 212 | if (moreTokens(tokens, index)) { 213 | nextToken = tokens[index++]; 214 | } else { 215 | break; 216 | } 217 | } 218 | } 219 | 220 | private void processAliasedMultiTables(final String[] tokens, int index, String currentToken) { 221 | String nextNextToken = null; 222 | if (moreTokens(tokens, index)) { 223 | nextNextToken = tokens[index++]; 224 | } 225 | 226 | if (shouldProcessMultipleTables(nextNextToken)) { 227 | while (moreTokens(tokens, index) && nextNextToken.equals(TOKEN_COMMA)) { 228 | if (moreTokens(tokens, index)) { 229 | currentToken = tokens[index++]; 230 | } 231 | if (moreTokens(tokens, index)) { 232 | index++; 233 | } 234 | if (moreTokens(tokens, index)) { 235 | nextNextToken = tokens[index++]; 236 | } 237 | considerInclusion(currentToken); 238 | } 239 | } 240 | } 241 | 242 | private boolean shouldProcessMultipleTables(final String nextToken) { 243 | return nextToken != null && nextToken.equals(TOKEN_COMMA); 244 | } 245 | 246 | private boolean moreTokens(final String[] tokens, int index) { 247 | return index < tokens.length; 248 | } 249 | 250 | private void considerInclusion(final String token) { 251 | if (!ignored.contains(token.toLowerCase()) && !this.tables.containsKey(token.toLowerCase())) { 252 | this.tables.put(token.toLowerCase(), token); 253 | } 254 | } 255 | 256 | /** 257 | * parser tables 258 | * 259 | * @return table names extracted out of sql 260 | */ 261 | public Collection tables() { 262 | return new HashSet<>(this.tables.values()); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.1.7.RELEASE 10 | 11 | 12 | 13 | top.rizon.springbestpractice 14 | spring-best-practice 15 | 0.0.1-SNAPSHOT 16 | 17 | pom 18 | spring-best-practice 19 | Spring Boot Best Practice 20 | 21 | 22 | common 23 | web 24 | auth 25 | dao 26 | 27 | 28 | 29 | 1.8 30 | Greenwich.SR2 31 | 3.0.7.1 32 | 1.2.0.CR1 33 | 34 | 35 | 36 | scm:git:https://github.com/othorizon/spring-best-practices.git 37 | https://github.com/othorizon/spring-best-practices 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-log4j2 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-cache 50 | 51 | 52 | spring-boot-starter-logging 53 | org.springframework.boot 54 | 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-web 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-aop 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-configuration-processor 68 | true 69 | 70 | 71 | org.projectlombok 72 | lombok 73 | true 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-starter-test 78 | test 79 | 80 | 81 | org.junit.vintage 82 | junit-vintage-engine 83 | 84 | 85 | 86 | 87 | 88 | 89 | com.baomidou 90 | mybatis-plus-boot-starter 91 | ${mybatisplus.version} 92 | 93 | 94 | 95 | org.jetbrains 96 | annotations 97 | compile 98 | 99 | 100 | com.google.code.gson 101 | gson 102 | 103 | 104 | com.google.guava 105 | guava 106 | 107 | 108 | com.github.ben-manes.caffeine 109 | caffeine 110 | 111 | 112 | org.apache.commons 113 | commons-lang3 114 | 115 | 116 | org.apache.commons 117 | commons-collections4 118 | 119 | 120 | org.jodd 121 | jodd-http 122 | 123 | 124 | joda-time 125 | joda-time 126 | 127 | 128 | com.alibaba 129 | fastjson 130 | 131 | 132 | com.jayway.jsonpath 133 | json-path 134 | 135 | 136 | org.flywaydb 137 | flyway-core 138 | 139 | 140 | com.github.pagehelper 141 | pagehelper-spring-boot-starter 142 | 143 | 144 | 145 | org.mapstruct 146 | mapstruct-jdk8 147 | 148 | 149 | org.mapstruct 150 | mapstruct-processor 151 | provided 152 | 153 | 154 | com.google.code.findbugs 155 | annotations 156 | 157 | 158 | 159 | 160 | com.h2database 161 | h2 162 | runtime 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | org.springframework.cloud 172 | spring-cloud-dependencies 173 | ${spring-cloud.version} 174 | pom 175 | import 176 | 177 | 178 | 179 | com.baomidou 180 | mybatis-plus-boot-starter 181 | ${mybatisplus.version} 182 | 183 | 184 | 185 | org.jetbrains 186 | annotations 187 | 16.0.3 188 | compile 189 | 190 | 191 | com.google.code.gson 192 | gson 193 | 2.7 194 | 195 | 196 | com.google.guava 197 | guava 198 | 19.0 199 | 200 | 201 | org.apache.commons 202 | commons-lang3 203 | 3.4 204 | 205 | 206 | org.apache.commons 207 | commons-collections4 208 | 4.1 209 | 210 | 211 | org.jodd 212 | jodd-http 213 | 3.9.1 214 | 215 | 216 | joda-time 217 | joda-time 218 | 2.9.4 219 | 220 | 221 | com.alibaba 222 | fastjson 223 | 1.2.58 224 | 225 | 226 | com.jayway.jsonpath 227 | json-path 228 | 2.4.0 229 | 230 | 231 | com.github.pagehelper 232 | pagehelper-spring-boot-starter 233 | 1.2.3 234 | 235 | 236 | 237 | org.mapstruct 238 | mapstruct-jdk8 239 | ${org.mapstruct.version} 240 | 241 | 242 | org.mapstruct 243 | mapstruct-processor 244 | ${org.mapstruct.version} 245 | provided 246 | 247 | 248 | 249 | com.google.code.findbugs 250 | annotations 251 | 3.0.1 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | org.apache.maven.plugins 263 | maven-compiler-plugin 264 | 3.7.0 265 | 266 | ${java.version} 267 | ${java.version} 268 | 269 | 270 | 271 | org.springframework.boot 272 | spring-boot-maven-plugin 273 | 2.0.0.RELEASE 274 | 275 | 276 | org.codehaus.mojo 277 | buildnumber-maven-plugin 278 | 1.4 279 | 280 | 281 | 282 | org.codehaus.mojo 283 | properties-maven-plugin 284 | 1.0.0 285 | 286 | 287 | 288 | org.codehaus.mojo 289 | exec-maven-plugin 290 | 1.6.0 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | aliyunmaven 299 | aliyunmaven 300 | https://maven.aliyun.com/repository/public 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /web/deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | 3 | MAINTAINER rizon 4 | LABEL site="https://github.com/othorizon/spring-best-practices" 5 | 6 | VOLUME /data 7 | WORKDIR /data 8 | 9 | COPY conf ./conf 10 | COPY lib ./lib 11 | 12 | # replace placeholder by package_jar.sh 13 | EXPOSE ${server_port} 14 | 15 | CMD java -Dspring.config.additional-location=file:./conf/application.properties,file:./conf/application.yml -jar ./lib/${jar_name} 2>&1 -------------------------------------------------------------------------------- /web/deploy/bin/app.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export set LC_ALL='en_US.UTF-8' 4 | 5 | #===== 6 | # replace placeholder by package_jar.sh 7 | jar_name=${jar_name} 8 | server_port=${server_port} 9 | logdir=${log_dir} 10 | #===== 11 | 12 | PRG="$0" 13 | BIN_HOME=`cd $(dirname "$PRG"); pwd` 14 | APP_HOME=`cd ${BIN_HOME}/..;pwd` 15 | jar_path="${APP_HOME}/lib" 16 | #LOG_DIR is log4j dir 17 | JVM_OPTS="-DLOG_DIR=${logdir} -Duser.timezone=GMT+08 -server -Xms6G -Xmx6G -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -Xloggc:${logdir}/gc.log -XX:-PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSCompactAtFullCollection -XX:+CMSParallelRemarkEnabled -XX:+HeapDumpOnOutOfMemoryError" 18 | #JVM_OPTS="-Duser.timezone=GMT+08 -server" 19 | startlog="${logdir}/console.log" 20 | server_url=http://localhost:$server_port 21 | 22 | mkdir -p $logdir 23 | 24 | #===== 25 | 26 | ping_server(){ 27 | 28 | while [[ true ]] 29 | do 30 | echo waiting server $server_url ... 31 | urlstatus=$(curl -s -m 5 -IL $server_url |grep HTTP) 32 | if [ "${urlstatus}" != "" ];then 33 | echo "${server_url} is ONLINE" 34 | break 35 | fi 36 | sleep 3 37 | done 38 | } 39 | 40 | get_pid(){ 41 | pid=`ps -ef | grep -v grep | grep "${jar_path}/${jar_name}" | awk '{print $2}'` 42 | echo $pid 43 | } 44 | 45 | process_is_running(){ 46 | pid=`get_pid` 47 | if [ -z $pid ] 48 | then 49 | echo 1 50 | else 51 | echo 0 52 | fi 53 | } 54 | 55 | start() { 56 | pid=`get_pid` 57 | if test `process_is_running` -eq 0 58 | then 59 | echo "WARN:process is running,pid is $pid" 60 | exit 1 61 | else 62 | echo "Starting server: " 63 | echo "setsid java ${JVM_OPTS} -Dspring.config.additional-location=file:${APP_HOME}/conf/application.properties,file:${APP_HOME}/conf/application.yml -jar ${jar_path}/${jar_name} >${startlog} 2>&1 &" 64 | # cd app_home for log relative path 65 | cd $APP_HOME 66 | setsid java ${JVM_OPTS} -Dspring.config.additional-location=file:${APP_HOME}/conf/application.properties,file:${APP_HOME}/conf/application.yml -jar ${jar_path}/${jar_name} >${startlog} 2>&1 & 67 | sleep 2s 68 | pid=`get_pid` 69 | if test `process_is_running` -eq 0 70 | then 71 | echo "start success! pid is $pid" 72 | ping_server 73 | else 74 | echo "start fail." 75 | fi 76 | fi 77 | } 78 | 79 | stop() { 80 | pid=`get_pid` 81 | if test `process_is_running` -eq 0 82 | then 83 | echo "stopping..." 84 | pid=`get_pid` 85 | kill -9 $pid 86 | if test `process_is_running` -eq 0 87 | then 88 | echo "stop fail" 89 | else 90 | echo "stop success" 91 | fi 92 | else 93 | echo "WARN:process is not exist." 94 | fi 95 | } 96 | 97 | restart() { 98 | stop 99 | start 100 | } 101 | 102 | rh_status() { 103 | pid=`get_pid` 104 | if test `process_is_running` -eq 0 105 | then 106 | echo "process is running,pid is $pid" 107 | else 108 | echo "process is not running" 109 | fi 110 | RETVAL=$? 111 | return $RETVAL 112 | } 113 | 114 | #===== 115 | 116 | case "$1" in 117 | start) 118 | start 119 | ;; 120 | stop) 121 | stop 122 | ;; 123 | restart) 124 | restart 125 | ;; 126 | status) 127 | rh_status 128 | ;; 129 | pid) 130 | get_pid 131 | ;; 132 | *) 133 | echo $"Usage: $0 {start|stop|status|restart|pid}" 134 | exit 1 135 | esac 136 | 137 | -------------------------------------------------------------------------------- /web/deploy/conf/application-test.yml: -------------------------------------------------------------------------------- 1 | # test conf 2 | 3 | # 该配置文件打包时会被复制作为spring程序的外部配置文件加载,会覆盖jar包内部的同key配置,可查阅 spring配置文件加载顺序 相关文档 4 | # 本案例中会在打包时把目标目录的该配置文件改名为 'application.yml' 目的是为了不再去明确的指定 `spring.profiles.active` 5 | 6 | logging: 7 | # 该示例配置文件会写出日志文件,配置中指定了日志目录,目录从系统变量读取,系统变量的值是在打包时从application.yml中读取写入app.sh中的 8 | config: classpath:log4j2-example.xml -------------------------------------------------------------------------------- /web/deploy/package_jar.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | if [ $1 == "false" ];then 5 | echo skip packageConf; 6 | exit 0 7 | fi 8 | 9 | PRG=$0 10 | workdir=`cd $(dirname "$PRG"); pwd` 11 | jarName=$2 12 | serverPort=$3 13 | logDir=$4 14 | env=$5 15 | # 打包方式 tar 包 或者 docker 镜像 16 | type=${6-"tar"} 17 | 18 | if [ -z $env ];then 19 | echo env is emty 20 | exit 1 21 | fi 22 | 23 | #========= 24 | 25 | cd target 26 | mkdir compose 27 | mkdir compose/bin 28 | mkdir compose/lib 29 | mkdir compose/conf 30 | 31 | cp $jarName.jar compose/lib/ 32 | cp ../deploy/bin/app.sh compose/bin/ 33 | cp ../deploy/conf/application-$env.yml compose/conf/application.yml 34 | 35 | # sed命令在linux和macos的语法不同 macos 强制需要 -i 指定备份,该写法可以兼容两个系统 36 | sed -i".bak" "s/\${jar_name}/$jarName.jar/g" compose/bin/app.sh 37 | sed -i".bak" "s/\${server_port}/$serverPort/g" compose/bin/app.sh 38 | sed -i".bak" "s:\${log_dir}:$logDir:g" compose/bin/app.sh 39 | rm compose/bin/app.sh.bak 40 | 41 | 42 | if [ $type == "tar" ]; then 43 | 44 | cd compose 45 | tar -czvf ../$jarName.tar.gz . 46 | 47 | elif [ $type == "docker" ]; then 48 | 49 | cp ../deploy/Dockerfile compose/ 50 | sed -i".bak" "s:\${server_port}:$serverPort:g" compose/Dockerfile 51 | sed -i".bak" "s/\${jar_name}/$jarName.jar/g" compose/Dockerfile 52 | rm compose/Dockerfile.bak 53 | 54 | cd compose 55 | docker build -t rizon/spring-best-practice . 56 | 57 | else 58 | echo not support type: $type 59 | fi 60 | -------------------------------------------------------------------------------- /web/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-best-practice 7 | top.rizon.springbestpractice 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | spring-best-practice-web 12 | 13 | 14 | 15 | false 16 | 17 | test 18 | tar 19 | 20 | 21 | 22 | 23 | top.rizon.springbestpractice 24 | common 25 | ${project.version} 26 | 27 | 28 | top.rizon.springbestpractice 29 | auth 30 | ${project.version} 31 | 32 | 33 | top.rizon.springbestpractice 34 | dao 35 | ${project.version} 36 | 37 | 38 | 39 | ${project.artifactId}-${project.version} 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 2.0.0.RELEASE 45 | 46 | 47 | org.codehaus.mojo 48 | buildnumber-maven-plugin 49 | 50 | 51 | validate 52 | 53 | create 54 | 55 | 56 | 57 | 58 | false 59 | false 60 | 61 | timestamp 62 | buildNumber 63 | 64 | 65 | 66 | 67 | org.codehaus.mojo 68 | properties-maven-plugin 69 | 70 | 71 | initialize 72 | 73 | read-project-properties 74 | write-project-properties 75 | 76 | 77 | 78 | ${project.basedir}/src/main/resources/application.yml 79 | 80 | ${project.build.directory}/project.properties 81 | 82 | 83 | 84 | 85 | 86 | exec-maven-plugin 87 | org.codehaus.mojo 88 | 89 | 90 | package 91 | 92 | exec 93 | 94 | 95 | 96 | 97 | ${basedir}/deploy/package_jar.sh 98 | 99 | ${packageConf} 100 | ${project.build.finalName} 101 | 102 | ${port} 103 | ${logDir} 104 | ${confEnv} 105 | ${buildType} 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author Rizon 8 | */ 9 | @SpringBootApplication(scanBasePackages = "top.rizon.springbestpractice") 10 | public class DemoApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(DemoApplication.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/AppInitConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.context.event.ApplicationReadyEvent; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * 程序启动后的初始化配置 12 | * 13 | * @author Rizon 14 | * @date 2019-08-27 15 | */ 16 | @Slf4j 17 | @Component 18 | public class AppInitConfig implements ApplicationListener { 19 | @Override 20 | public void onApplicationEvent(ApplicationReadyEvent event) { 21 | Map beans = event.getApplicationContext().getBeansOfType(InitConfig.class); 22 | for (Map.Entry configEntry : beans.entrySet()) { 23 | log.info("start init config:{}", configEntry.getKey()); 24 | try { 25 | configEntry.getValue().init(); 26 | } catch (Exception ex) { 27 | log.error(String.format("config '%s' init failed", configEntry.getKey()), ex); 28 | System.exit(1); 29 | return; 30 | } 31 | } 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/AsyncThreadPoolConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.scheduling.annotation.EnableAsync; 8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 9 | 10 | import java.util.concurrent.ThreadPoolExecutor; 11 | 12 | /** 13 | * @author Rizon 14 | * @date 2019/12/19 15 | */ 16 | @EnableAsync 17 | @Configuration 18 | @ConfigurationProperties("async-conf") 19 | @Data 20 | public class AsyncThreadPoolConfig { 21 | /** 22 | * 核心线程数(默认线程数) 23 | */ 24 | private Integer corePoolSize = 20; 25 | /** 26 | * 最大线程数 27 | */ 28 | private Integer maxPoolSize = 100; 29 | /** 30 | * 允许线程空闲时间(单位:默认为秒) 31 | */ 32 | private Integer keepAliveTime = 10; 33 | /** 34 | * 缓冲队列大小 35 | */ 36 | private Integer queueCapacity = 200; 37 | /** 38 | * 线程池名前缀 39 | */ 40 | private String threadNamePrefix = "Async-Task-"; 41 | 42 | @Bean 43 | public ThreadPoolTaskExecutor asyncTaskExecutor() { 44 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 45 | executor.setCorePoolSize(corePoolSize); 46 | executor.setMaxPoolSize(maxPoolSize); 47 | executor.setQueueCapacity(queueCapacity); 48 | executor.setKeepAliveSeconds(keepAliveTime); 49 | executor.setThreadNamePrefix(threadNamePrefix); 50 | 51 | // 线程池对拒绝任务的处理策略 52 | // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 53 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 54 | // 初始化 55 | executor.initialize(); 56 | return executor; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/CachingConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config; 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine; 4 | import lombok.Setter; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.cache.CacheManager; 8 | import org.springframework.cache.annotation.EnableCaching; 9 | import org.springframework.cache.caffeine.CaffeineCacheManager; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.Primary; 13 | 14 | import java.time.Duration; 15 | 16 | /** 17 | * 缓存配置 18 | * 19 | * @author Rizon 20 | * @date 2019-08-28 21 | */ 22 | @Configuration 23 | @EnableCaching 24 | @ConditionalOnProperty(value = "cache.enable", havingValue = "true") 25 | @ConfigurationProperties(prefix = "cache") 26 | public class CachingConfig { 27 | @Setter 28 | private Duration expireDuration = Duration.ofMinutes(3); 29 | 30 | /** 31 | * expireAfterWrite 缓存管理器 32 | */ 33 | @Primary 34 | @Bean("expireCacheManager") 35 | public CacheManager cacheManager() { 36 | CaffeineCacheManager manager = new CaffeineCacheManager(); 37 | manager.setCaffeine(Caffeine.newBuilder() 38 | .expireAfterWrite(expireDuration) 39 | ); 40 | return manager; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/InitConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config; 2 | 3 | /** 4 | * @author Rizon 5 | * @date 2019-08-27 6 | */ 7 | public interface InitConfig { 8 | /** 9 | * 程序启动完成后会调用该方法 10 | * 抛出异常则会导致程序终止 11 | * 12 | * @throws Exception 13 | */ 14 | void init() throws Exception; 15 | } 16 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/ServerInitConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.stereotype.Component; 10 | import top.rizon.springbestpractice.common.model.response.Response; 11 | 12 | /** 13 | * @author Rizon 14 | * @date 2019/12/13 15 | */ 16 | @Component 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | public class ServerInitConfig implements InitializingBean { 20 | private final BaseServerConfig config; 21 | 22 | 23 | @Override 24 | public void afterPropertiesSet() throws Exception { 25 | log.info("setCloseExStack:{}", config.isCloseExStack()); 26 | Response.setCloseExStack(config.isCloseExStack()); 27 | } 28 | 29 | @Configuration 30 | @ConfigurationProperties(prefix = "base-server") 31 | @Data 32 | public static class BaseServerConfig { 33 | /** 34 | * 关闭response的errMessage输出, 35 | * 生产环境面向前端的服务建议关闭该配置,防止泄漏信息 36 | */ 37 | private boolean closeExStack; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Primary; 7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 10 | import top.rizon.springbestpractice.common.cache.CacheHandlerInterceptor; 11 | import top.rizon.springbestpractice.common.cache.RequestScopedCacheManager; 12 | import top.rizon.springbestpractice.common.model.dto.AuthUser; 13 | import top.rizon.springbestpractice.common.utils.AuthUtil; 14 | import top.rizon.springbestpractice.common.utils.TraceUtils; 15 | 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | 19 | /** 20 | * @author Rizon 21 | * @date 2019/12/13 22 | */ 23 | @Configuration 24 | @Slf4j 25 | public class WebConfig implements WebMvcConfigurer { 26 | @Bean("requestScopedCacheManager") 27 | @Primary 28 | public RequestScopedCacheManager requestScopedCacheManager() { 29 | return new RequestScopedCacheManager(); 30 | } 31 | 32 | @Bean 33 | public CacheHandlerInterceptor cacheHandlerInterceptor() { 34 | return new CacheHandlerInterceptor(requestScopedCacheManager()); 35 | } 36 | 37 | @Bean 38 | public RequestInterceptor requestInterceptor() { 39 | return new RequestInterceptor(); 40 | } 41 | 42 | @Override 43 | public void addInterceptors(InterceptorRegistry registry) { 44 | log.info("register requestInterceptor"); 45 | registry.addInterceptor(requestInterceptor()).addPathPatterns("/**"); 46 | log.info("register requestScopedCacheManager"); 47 | registry.addInterceptor(cacheHandlerInterceptor()).addPathPatterns("/**"); 48 | } 49 | 50 | public static class RequestInterceptor extends HandlerInterceptorAdapter { 51 | @Override 52 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 53 | AuthUser user = AuthUtil.getAuthUser(request); 54 | TraceUtils.beginTaskTrace(String.format("%s-%s", 55 | TraceUtils.randomTraceId(), user == null ? null : user.getId())); 56 | return true; 57 | } 58 | 59 | @Override 60 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 61 | TraceUtils.endTaskTrace(); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/auth/AuthWebConfig.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config.auth; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | import top.rizon.springbestpractice.common.handler.AbstractAuthHandler; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * 认证拦截配置 18 | * 19 | * @author Rizon 20 | * @date 2019/12/3 21 | */ 22 | @ConditionalOnProperty(value = "auth.enable", havingValue = "true", matchIfMissing = true) 23 | @Configuration 24 | @RequiredArgsConstructor 25 | @Slf4j 26 | public class AuthWebConfig implements WebMvcConfigurer { 27 | private final AbstractAuthHandler authHandler; 28 | private final AuthConf authConf; 29 | 30 | @Override 31 | public void addInterceptors(InterceptorRegistry registry) { 32 | log.info("register auth interceptor:{}", authHandler.getClass()); 33 | registry.addInterceptor(authHandler) 34 | .excludePathPatterns(authConf.getExcludePath()); 35 | } 36 | 37 | 38 | @Configuration 39 | @ConfigurationProperties(prefix = "auth", ignoreInvalidFields = true) 40 | @Data 41 | public static class AuthConf { 42 | private List excludePath = new ArrayList<>(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/auth/RestTemplateAuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config.auth; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.jetbrains.annotations.Nullable; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.http.HttpRequest; 8 | import org.springframework.stereotype.Component; 9 | import top.rizon.springbestpractice.common.utils.AuthUtil; 10 | import top.rizon.springbestpractice.common.utils.http.BaseAuthHeaderHttpRequestInterceptor; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * 拦截restTemplate请求,注入认证信息的demo 16 | * 17 | * @author Rizon 18 | * @date 2019/12/3 19 | */ 20 | @ConditionalOnProperty(value = "auth.enable", havingValue = "true", matchIfMissing = true) 21 | @Component 22 | @RequiredArgsConstructor 23 | @Slf4j 24 | public class RestTemplateAuthInterceptor extends BaseAuthHeaderHttpRequestInterceptor { 25 | private final AuthWebConfig.AuthConf authConf; 26 | 27 | @Override 28 | public void process(HttpRequest request) { 29 | log.info("http request interceptor"); 30 | request.getHeaders().add("AUTH-TOKEN", AuthUtil.authUserOrFailed().getToken()); 31 | } 32 | 33 | 34 | @Override 35 | @Nullable 36 | public List includePathPatterns() { 37 | //返回null表示全部生效 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/config/demo/DemoInit.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.config.demo; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.stereotype.Component; 5 | import top.rizon.springbestpractice.web.config.InitConfig; 6 | 7 | /** 8 | * @author Rizon 9 | * @date 2019/12/25 10 | */ 11 | @Component 12 | @Slf4j 13 | public class DemoInit implements InitConfig { 14 | @Override 15 | public void init() throws Exception { 16 | log.info("this is demo init"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/controller/CacheExampleController.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import top.rizon.springbestpractice.common.model.response.Response; 9 | import top.rizon.springbestpractice.web.service.CacheExampleService; 10 | 11 | /** 12 | * 缓存的几种写法 13 | * 使用缓存是要保证对缓存数据的操作是无副作用的,防止污染缓存数据 14 | * 15 | * @author Rizon 16 | * @date 2019/12/25 17 | */ 18 | @RestController 19 | @RequestMapping("cache") 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | public class CacheExampleController { 23 | private final CacheExampleService service; 24 | 25 | /** 26 | * 使用spring的缓存注解 27 | * 一个请求域的缓存,只在当前请求中使用 28 | * 29 | * @return 30 | */ 31 | @GetMapping("byAnnotation") 32 | public Response byAnnotation() { 33 | log.info("requestScoped cache:{}", service.requestScopedByAnnotation()); 34 | return Response.success(service.requestScopedByAnnotation()); 35 | } 36 | 37 | /** 38 | * 到期失效的缓存 39 | * 不使用spring的缓存注解 40 | * 41 | * @return 42 | */ 43 | @GetMapping("expireCache") 44 | public Response expireCache() { 45 | log.info("expireCache:{}", service.expireCache()); 46 | return Response.success(service.expireCache()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/controller/DemoController.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.controller; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | import top.rizon.springbestpractice.common.constant.DateFormatType; 9 | import top.rizon.springbestpractice.common.model.response.Response; 10 | import top.rizon.springbestpractice.common.utils.DataDate; 11 | import top.rizon.springbestpractice.web.service.AopExampleService; 12 | 13 | /** 14 | * @author Rizon 15 | * @date 2019/12/26 16 | */ 17 | @RestController 18 | @RequestMapping("demo") 19 | @RequiredArgsConstructor 20 | @Slf4j 21 | public class DemoController { 22 | private final AopExampleService aopService; 23 | 24 | @GetMapping("aop") 25 | public ResponseEntity aopDemo() { 26 | return ResponseEntity.ok(aopService.sleepMethod()); 27 | } 28 | 29 | @GetMapping("badRequest") 30 | public ResponseEntity badRequest() { 31 | return ResponseEntity.badRequest().body("http status 400"); 32 | } 33 | 34 | /** 35 | * 演示了jsonCreator的使用 36 | * 和枚举实现的简单策略模式,避免过于复杂的if-else 37 | *

38 | * 示例: 39 | *

40 | * POST /demo/formatDate 41 | *

42 | * { 43 | * "date":"2019-10-10", 44 | * "formatType":1 45 | * } 46 | */ 47 | @PostMapping("formatDate") 48 | public Response formatDate(@RequestBody DateFormatParam param) { 49 | return Response.success(param.getFormatType().format(param.getDate())); 50 | } 51 | 52 | @Data 53 | public static class DateFormatParam { 54 | /** 55 | * 通过配置jsonCreator指定jackson转换对象时的构造函数 56 | */ 57 | private DataDate date; 58 | /** 59 | * 枚举类也可以直接作为参数接收, 60 | * 在没有特殊处理情况下会使用name去转换 61 | * 但是可以通过配置jsonCreator 去指定jackson的构造函数 62 | */ 63 | private DateFormatType formatType; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/controller/HttpController.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.core.ParameterizedTypeReference; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import top.rizon.springbestpractice.common.model.response.Response; 10 | import top.rizon.springbestpractice.common.utils.http.SimpleRestTemplateUtils; 11 | import top.rizon.springbestpractice.web.config.auth.RestTemplateAuthInterceptor; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | 15 | /** 16 | * http请求演示 17 | * 18 | * @author Rizon 19 | * @date 2019/12/25 20 | */ 21 | @RestController 22 | @RequestMapping("http") 23 | @RequiredArgsConstructor 24 | @Slf4j 25 | public class HttpController { 26 | 27 | /** 28 | * 请求服务时拦截器会注入认证信息 29 | * 30 | * @see RestTemplateAuthInterceptor 31 | */ 32 | @GetMapping 33 | public Response restTemplate(HttpServletRequest request) { 34 | return SimpleRestTemplateUtils.doGetRes( 35 | request.getRequestURL() + "/result", null, null, 36 | new ParameterizedTypeReference>() { 37 | }); 38 | } 39 | 40 | @GetMapping("result") 41 | public Response requestResult() { 42 | return Response.success("this is demo result"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/controller/ServerHealthController.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.controller; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.format.annotation.DateTimeFormat; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import top.rizon.springbestpractice.common.utils.DateTimeUtils; 13 | 14 | import java.util.Date; 15 | 16 | /** 17 | * 健康检查 18 | * 19 | * @author Rizon 20 | * @date 2019/12/25 21 | */ 22 | @Slf4j 23 | @RestController 24 | @RequestMapping("/health") 25 | @RequiredArgsConstructor 26 | public class ServerHealthController { 27 | private final VersionInfo versionInfo; 28 | 29 | 30 | /** 31 | * 服务心跳检测接口 32 | * 33 | * @return 34 | */ 35 | @GetMapping(value = "/ping") 36 | public String ping() { 37 | return "pong"; 38 | } 39 | 40 | 41 | @GetMapping 42 | public ServerHealthResponse healthController() { 43 | return new ServerHealthResponse(); 44 | } 45 | 46 | @GetMapping("version") 47 | public VersionInfo buildVersion() { 48 | return versionInfo; 49 | } 50 | 51 | 52 | @Data 53 | @Component 54 | @ConfigurationProperties(prefix = "project-build-version-info") 55 | public static class VersionInfo { 56 | private String version; 57 | private String buildTimestamp; 58 | private String scmVersion; 59 | } 60 | 61 | @Data 62 | public static class ServerHealthResponse { 63 | private int status = 200; 64 | private String message = "success"; 65 | @DateTimeFormat(pattern = DateTimeUtils.YYYYMMDDHHMMSS) 66 | private Date serverTime = new Date(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/controller/SqlExampleController.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.validation.annotation.Validated; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import top.rizon.springbestpractice.common.model.request.PageParam; 11 | import top.rizon.springbestpractice.common.model.response.PageResponse; 12 | import top.rizon.springbestpractice.common.model.response.Response; 13 | import top.rizon.springbestpractice.common.utils.DataDate; 14 | import top.rizon.springbestpractice.web.service.SqlExampleService; 15 | 16 | /** 17 | * @author Rizon 18 | * @date 2019/12/27 19 | */ 20 | @RestController 21 | @RequestMapping("demo/sql") 22 | @RequiredArgsConstructor 23 | @Slf4j 24 | public class SqlExampleController { 25 | private final SqlExampleService service; 26 | 27 | /** 28 | * 该案例中,历史数据为按日期拆分的表(history_20191227),通过动态表名实现 按日期查询分表 29 | *

30 | * 演示了动态表名和PageHelper 31 | * 32 | * @param date 默认值为本案例提供的演示表 33 | * @param pageParam nullable 为null则不分页 34 | */ 35 | @GetMapping("queryHistory") 36 | public Response queryDateTable(@RequestParam(required = false, defaultValue = "2019-12-27") DataDate date, @Validated PageParam pageParam) { 37 | if (pageParam != null) { 38 | return PageResponse.success(service.selectHistoryByDate(date, pageParam), pageParam); 39 | } else { 40 | return Response.success(service.selectHistoryByDate(date, null)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.controller; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.baomidou.mybatisplus.core.metadata.IPage; 5 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; 6 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.validation.annotation.Validated; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import top.rizon.springbestpractice.common.aspect.AutoRepairPageAspect; 15 | import top.rizon.springbestpractice.common.model.request.PageParam; 16 | import top.rizon.springbestpractice.common.model.response.PageResponse; 17 | import top.rizon.springbestpractice.common.model.response.Response; 18 | import top.rizon.springbestpractice.dao.helper.UserHelper; 19 | import top.rizon.springbestpractice.dao.po.User; 20 | import top.rizon.springbestpractice.web.model.WebObjMapper; 21 | import top.rizon.springbestpractice.web.model.param.UserQueryParam; 22 | import top.rizon.springbestpractice.web.model.vo.UserVo; 23 | 24 | import java.util.List; 25 | import java.util.stream.Collectors; 26 | 27 | /** 28 | * @author Rizon 29 | * @date 2019/12/25 30 | */ 31 | @RestController 32 | @RequestMapping("user") 33 | @RequiredArgsConstructor 34 | @Slf4j 35 | public class UserController { 36 | private final UserHelper userHelper; 37 | private final WebObjMapper webObjMapper; 38 | 39 | /** 40 | *

41 | * 分页页码自动纠正AOP, 42 | * 当页码大于数据真实页码时会纠正为最后一页的数据,这可以解决前端分页展示删除最后一页的最后一条数据时刷新后为无数据的空白页的问题 43 | *

44 | * 45 | * @see AutoRepairPageAspect 46 | */ 47 | @GetMapping("list") 48 | public Response> list(@Validated UserQueryParam param) { 49 | log.info("request list user,param:{}", param); 50 | PageParam pageParam = param.getPageParam(); 51 | LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() 52 | .like(StringUtils.isNotEmpty(param.getEmailLike()), User::getEmail, param.getEmailLike()); 53 | 54 | if (pageParam != null) { 55 | IPage result = userHelper.page(new Page<>(pageParam.getPage(), pageParam.getPageSize()), queryWrapper); 56 | return PageResponse.success( 57 | result.getRecords().stream().map(webObjMapper::toVo).collect(Collectors.toList()), 58 | pageParam.setTotalPage(result.getTotal()) 59 | .setTotalPage(result.getPages())); 60 | } else { 61 | return Response.success(userHelper.list(queryWrapper).stream() 62 | .map(webObjMapper::toVo).collect(Collectors.toList())); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/model/WebObjMapper.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.model; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.Mapping; 5 | import org.mapstruct.factory.Mappers; 6 | import top.rizon.springbestpractice.dao.po.User; 7 | import top.rizon.springbestpractice.web.model.vo.UserVo; 8 | 9 | /** 10 | * @author Rizon 11 | * @date 2019/12/25 12 | */ 13 | @Mapper(componentModel = "spring") 14 | public interface WebObjMapper { 15 | WebObjMapper INSTANCE = Mappers.getMapper(WebObjMapper.class); 16 | 17 | /** 18 | * 将用户转换为vo对象 19 | * 20 | * @param user 21 | * @return 22 | */ 23 | @Mapping(source = "token",target = "loginToken") 24 | UserVo toVo(User user); 25 | } 26 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/model/param/UserQueryParam.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.model.param; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import top.rizon.springbestpractice.common.model.request.BaseReqParam; 6 | 7 | /** 8 | * @author Rizon 9 | * @date 2019/12/25 10 | */ 11 | @EqualsAndHashCode(callSuper = true) 12 | @Data 13 | public class UserQueryParam extends BaseReqParam { 14 | private String emailLike; 15 | } 16 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/model/vo/CacheExampleVo.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.model.vo; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * @author Rizon 9 | * @date 2019/12/25 10 | */ 11 | @Data 12 | public class CacheExampleVo { 13 | private String uuid = UUID.randomUUID().toString(); 14 | private long timestamp = System.currentTimeMillis(); 15 | } 16 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/model/vo/UserVo.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.model.vo; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/25 9 | */ 10 | @Data 11 | public class UserVo { 12 | private Long id; 13 | @JsonProperty("userName") 14 | private String name; 15 | private Integer age; 16 | private String email; 17 | private String loginToken; 18 | } 19 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/service/AopExampleService.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.service; 2 | 3 | import lombok.SneakyThrows; 4 | import org.springframework.stereotype.Service; 5 | 6 | /** 7 | * @author Rizon 8 | * @date 2019/12/26 9 | */ 10 | @Service 11 | public class AopExampleService { 12 | 13 | @SneakyThrows 14 | public String sleepMethod() { 15 | Thread.sleep(5000); 16 | return "sleep 5s"; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/service/CacheExampleService.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.service; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.stereotype.Service; 9 | import top.rizon.springbestpractice.common.exception.BaseServerException; 10 | import top.rizon.springbestpractice.web.model.vo.CacheExampleVo; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.concurrent.ExecutionException; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * 缓存使用应当注意只对缓存数据做读操作,避免side-effect,防止污染缓存数据 20 | * 21 | * @author Rizon 22 | * @date 2019/12/25 23 | */ 24 | @Service 25 | @RequiredArgsConstructor 26 | @Slf4j 27 | public class CacheExampleService { 28 | private static final String MANUAL_CACHE_KEY = "MANUAL_CACHE_KEY"; 29 | private Cache> cache; 30 | 31 | @PostConstruct 32 | public void init() { 33 | cache = CacheBuilder.newBuilder() 34 | .maximumSize(5) 35 | .expireAfterWrite(5, TimeUnit.SECONDS) 36 | .build(); 37 | } 38 | 39 | /** 40 | * 与其他的基于Spring Aop代理的注解(如 @Transactional)相同, 41 | * 必须应用在public方法,而且不能在同类中自调用,否则代理无法生效 42 | */ 43 | @Cacheable(value = "byAnnotation", cacheManager = "requestScopedCacheManager") 44 | public List requestScopedByAnnotation() { 45 | log.info("init requestScoped cache data"); 46 | return Arrays.asList(new CacheExampleVo(), new CacheExampleVo()); 47 | } 48 | 49 | public List expireCache() { 50 | try { 51 | return cache.get(MANUAL_CACHE_KEY, () -> { 52 | log.info("init expireCache data"); 53 | return Arrays.asList(new CacheExampleVo(), new CacheExampleVo()); 54 | }); 55 | } catch (ExecutionException e) { 56 | throw new BaseServerException(e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/src/main/java/top/rizon/springbestpractice/web/service/SqlExampleService.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web.service; 2 | 3 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; 4 | import com.github.pagehelper.Page; 5 | import com.github.pagehelper.PageHelper; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | import top.rizon.springbestpractice.common.model.request.PageParam; 10 | import top.rizon.springbestpractice.common.utils.DataDate; 11 | import top.rizon.springbestpractice.dao.mapper.HistoryMapper; 12 | import top.rizon.springbestpractice.dao.po.HistoryPo; 13 | import top.rizon.springbestpractice.dao.utils.dynamictblname.DynamicTableNameUtils; 14 | import top.rizon.springbestpractice.web.controller.UserController; 15 | 16 | import java.util.List; 17 | 18 | /** 19 | * @author Rizon 20 | * @date 2019/12/27 21 | */ 22 | @Service 23 | @RequiredArgsConstructor 24 | @Slf4j 25 | public class SqlExampleService { 26 | private final HistoryMapper historyMapper; 27 | 28 | /** 29 | * 动态表名demo, 30 | * 查询指定日期的分表 31 | *

32 | * PageHelper实现的一种分页方式演示, 33 | * 与动态表名原理一样,都是threadLocal的全局变量注入到sql中 34 | *

35 | *

36 | * 不推荐PageHelper分页方式,仅作演示,mybatis-plus实现了分页逻辑 37 | * 38 | * @see UserController#list Mybatis-Plus的分页方式 39 | */ 40 | public List selectHistoryByDate(DataDate date, PageParam pageParam) { 41 | DynamicTableNameUtils.startHistoryTableName(date); 42 | 43 | Page page = null; 44 | if (pageParam != null) { 45 | page = PageHelper.startPage(pageParam.getPage(), pageParam.getPageSize(), true); 46 | } 47 | 48 | List historyPos = historyMapper.selectList(Wrappers.emptyWrapper()); 49 | 50 | if (page != null) { 51 | pageParam.setTotalPage(page.getPages()) 52 | .setTotalRecord(page.getTotal()); 53 | } 54 | 55 | //清理ThreadLocal 56 | DynamicTableNameUtils.endTableName(); 57 | PageHelper.clearPage(); 58 | return historyPos; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/src/main/resources/ValidationMessages.properties: -------------------------------------------------------------------------------- 1 | # 需要转换为native编码,否则会乱码 2 | # page 3 | ## 页码(page)最小值为{value} 4 | min.page=\u9875\u7801(page)\u6700\u5c0f\u503c\u4e3a{value} 5 | ## 页大小(pageSize)最小值为{value} 6 | min.pageSize=\u9875\u5927\u5c0f(pageSize)\u6700\u5c0f\u503c\u4e3a{value} -------------------------------------------------------------------------------- /web/src/main/resources/ValidationMessages_zh_CN.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/othorizon/spring-best-practices/2f96434f8a66c99c569214d7d2d1b643f473fa26/web/src/main/resources/ValidationMessages_zh_CN.properties -------------------------------------------------------------------------------- /web/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | servlet: 4 | context-path: / 5 | 6 | # 健康接口的版本信息 7 | project-build-version-info: 8 | version: @project.version@ 9 | buildTimestamp: @timestamp@ 10 | scmVersion: @buildNumber@ 11 | 12 | spring: 13 | # 自动管理数据库版本 14 | flyway: 15 | enabled: true 16 | # sql文件位置 17 | locations: classpath:db/migration 18 | # flyway 的 clean 命令会删除指定 schema 下的所有 table,禁用 19 | cleanDisabled: true 20 | # 如果指定 schema 包含了其他表,但没有 flyway_schema_history 表的话, 在执行 flyway migrate 命令之前, 必须先执行 flyway baseline 命令. 21 | # 设置 spring.flyway.baseline-on-migrate 为 true 后, flyway 将在需要 baseline 的时候, 自动执行一次 baseline. 22 | baselineOnMigrate: true 23 | # 指定 baseline 的版本号,缺省值为 1, 等于或低于该版本号的 SQL 文件, migrate 的时候被忽略. 24 | ## 该配置可用于已经上线了的没有使用flyway的数据库的第一次初始化flyway场景: 25 | ## 线上数据库已经有表了,这时候可以新增一个baseline的sql文件其中包含当前线上数据库表的初始化sql,然后设置该参数与baseline sql的版本一致 26 | ## 当第一次上线flyway时,因为线上库中有其他表但是没有flyway_schema_history表, 27 | ## 因此flyway会创建该表并写入一条baseline的记录,而且因为baseline sql文件版本号小于等于该参数而不会被migrate执行。 28 | ## 而如果在一个全新的环境中启动初始化时,不会执行baseline操作,因此baseline sql文件会被migrate执行完成初始化操作 29 | baselineVersion: 2019.12.24.1 30 | outOfOrder: false 31 | # 需要 flyway 管控的 schema list, 缺省的话, 使用的时 datasource.url 直连上的那个 schema, 可以指定多个schema, 但仅会在第一个schema下建立 metadata 表, 也仅在第一个schema应用migration sql 脚本. 但flyway Clean 命令会依次在这些schema下都执行一遍. 32 | schemas: 33 | # Demo DataSource Config 34 | datasource: 35 | driver-class-name: org.h2.Driver 36 | url: jdbc:h2:mem:test 37 | username: root 38 | password: test 39 | jackson: 40 | date-format: yyyy-MM-dd HH:mm:ss 41 | time-zone: GMT+8 42 | 43 | # 该参数会在打包时写入启动脚本中,作为系统变量,log4j会读取该值,参考: log4j2-example.xml 44 | logDir: /tmp/log/ 45 | logging: 46 | config: classpath:log4j2.xml 47 | 48 | auth: 49 | exclude-path: 50 | - /error 51 | - /health/** 52 | - /demo/** 53 | -------------------------------------------------------------------------------- /web/src/main/resources/db/migration/V2019.12.24.1__baseline.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user 2 | ( 3 | id BIGINT(20) NOT NULL COMMENT '主键ID', 4 | name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', 5 | age INT(11) NULL DEFAULT NULL COMMENT '年龄', 6 | email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', 7 | token VARCHAR(50) NULL DEFAULT NULL COMMENT '登陆token', 8 | PRIMARY KEY (id) 9 | ); 10 | 11 | INSERT INTO user (id, name, age, email, token) 12 | VALUES (1, 'Jone', 18, 'test1@rizon.top', 'token-1'), 13 | (2, 'Jack', 20, 'test2@rizon.top', 'token-2'), 14 | (3, 'Tom', 28, 'test3@rizon.top', 'token-3'), 15 | (4, 'Sandy', 21, 'test4@rizon.top', 'token-4'), 16 | (5, 'Billie', 24, 'test5@rizon.top', 'token-5'); 17 | 18 | 19 | CREATE TABLE history_20191227 20 | ( 21 | id BIGINT(20) NOT NULL COMMENT '主键ID', 22 | data VARCHAR(255) NULL DEFAULT NULL COMMENT '历史数据', 23 | create_time timestamp NOT NULL COMMENT '创建时间', 24 | PRIMARY KEY (id) 25 | ); 26 | 27 | INSERT INTO history_20191227(id, data, create_time) 28 | VALUES (1, 'this is history 1', DATEADD('DAY', -5, current_timestamp())), 29 | (2, 'this is history 2', DATEADD('DAY', -4, current_timestamp())), 30 | (3, 'this is history 3', DATEADD('DAY', -3, current_timestamp())), 31 | (4, 'this is history 4', DATEADD('DAY', -2, current_timestamp())), 32 | (5, 'this is history 5', DATEADD('DAY', -1, current_timestamp())); -------------------------------------------------------------------------------- /web/src/main/resources/log4j2-example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | /data/log 10 | 11 | ${sys:LOG_DIR} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /web/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /web/src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | TOO_MANY_RESULTS_EXCEPTION=结果数量过多,期望1,实际{0} 2 | AUTH_FAILED=认证失败 3 | NOT_FOUND_AUTH_TOKEN=header {0} is empty,eg. {0}:token-1 -------------------------------------------------------------------------------- /web/src/main/resources/messages_zh_CN.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/othorizon/spring-best-practices/2f96434f8a66c99c569214d7d2d1b643f473fa26/web/src/main/resources/messages_zh_CN.properties -------------------------------------------------------------------------------- /web/src/test/java/top/rizon/springbestpractice/AppTest.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest 11 | { 12 | /** 13 | * Rigorous Test :-) 14 | */ 15 | @Test 16 | public void shouldAnswerWithTrue() 17 | { 18 | assertTrue( true ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/src/test/java/top/rizon/springbestpractice/web/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package top.rizon.springbestpractice.web; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class DemoApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------