项目架构经验之谈之SpringBoot单体应用架构分享(一)-项目框架篇
程序员文章站
2022-05-03 11:00:56
...
文章目录
1.为什么要写这篇文章
在这个java开发框架很多的年代里面,如何才能搭建一个易用的单体开发框架 (该文章只是本人的经验之谈,若感觉太菜,可以关闭本文)
2.单体应用的好处与坏处
好处
- 仅需要一个war或者jar,执行即可,无需再部署一个前端,例如现在java+vue,vue需要NGINX或APATHE去部署
坏处
- 不利于后期的版本递增,代码容易冗余
- 不利于多人开发,易冲突
- 一旦服务挂了,所有功能都无法使用
3.技术选项
本文的技术选项暂时[后期文章会持续的升级技术栈]
使用如下技术栈
- SpringBoot
- MyBatis Plus
- Druid
- MySql
- lombok
- swagger UI
- commons 工具插件
- hutool 工具插件
4.框架架构实战
4.1 pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cxl.fm</groupId>
<artifactId>spring-boot-cxl-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-cxl-starter</name>
<description>【SpringBoot-CXL-快速开发脚手架】</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<junit.version>4.12</junit.version>
<xstream.version>1.4.9</xstream.version>
<fastjson.version>1.2.15</fastjson.version>
<commons-io.version>2.5</commons-io.version>
<commons-fileupload.version>1.3.3</commons-fileupload.version>
<hibernate-validator.version>6.0.10.Final</hibernate-validator.version>
<metadata-extractor.version>2.6.2</metadata-extractor.version>
</properties>
<dependencies>
<!--web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.6.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--数据库 -->
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!--开发辅助 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.1</version>
</dependency>
<!-- 添加httpclient支持 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<!-- swgger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<!-- https://blog.csdn.net/chenwiehuang/article/details/83114641 -->
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.11</version>
</dependency>
<!-- commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.nervous/juint -->
<dependency>
<groupId>io.nervous</groupId>
<artifactId>juint</artifactId>
<version>0.1.0</version>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.2 启动类
@SpringBootApplication
@EnableAutoConfiguration
@EnableWebMvc
@ServletComponentScan(basePackages = "com.cxl.fm")
public class SpringBootCxlStarterMain implements ApplicationRunner {
@Value("${server.port}")
private String serverPort;
public static void main(String[] args) {
SpringApplication.run(SpringBootCxlStarterMain.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("api 接口启动完成 = http://localhost:"+serverPort);
}
}
4.2.1 启动回调设置
通过在启动类中,实现ApplicationRunner
接口,将会重写run
方法,该run方法即为启动后执行的方法
4.3 yml配置文件
# ================== server config ==================
server:
port: 8088
tomcat:
max-threads: 50000
max-connections: 50000000
uri-encoding: UTF-8
# =====================================================
# ================ spring datasource config
sql:
sqlIp: 127.0.0.1
sqlPort: 3306
sqlDbName: test
sqlUserName: root
sqlPassWord: root
spring:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://${sql.sqlIp}:${sql.sqlPort}/${sql.sqlDbName}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
username: ${sql.sqlUserName}
password: ${sql.sqlPassWord}
type: com.alibaba.druid.pool.DruidDataSource
druid:
stat-view-servlet:
url-pattern: /druid/*
reset-enable: true
login-username: admin
login-password: admin
validation-query: SELECT 'x'
main:
allow-bean-definition-overriding: true
# hymeleaf
thymeleaf:
mode: LEGACYHTML5
cache: false
redis:
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
# http
http:
encoding:
force: true
charset: UTF-8
enabled: true
mybatis-plus:
# MyBatis 配置文件位置,如果您有单独的 MyBatis 配置,请将其路径配置到 configLocation 中。
# config-location: classpath:mybatis-config.xml
# MyBatis Mapper 所对应的 XML 文件位置,如果您在 Mapper 中有自定义方法
mapper-locations: classpath:mapper/*.xml
# MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名 实体扫描,多个package用逗号或者分号分隔
type-aliases-package: com.zqg.zhuzhu.po
# # 配置扫描通用枚举 # 支持统配符 * 或者 ; 分割
#type-enums-package: com.abbottliu.sys.enums,com.abbottliu.enums
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
check-config-location: true
# ExecutorType.SIMPLE:该执行器类型不做特殊的事情,为每个语句的执行创建一个新的预处理语句(PreparedStatement)
# ExecutorType.REUSE:该执行器类型会复用预处理语句(PreparedStatement)
# ExecutorType.BATCH:该执行器类型会批量执行所有的更新语句
executor-type: simple
configuration:
# 是否开启自动驼峰命名规则(camel case)映射
map-underscore-to-camel-case: true
#配置JdbcTypeForNull, oracle数据库必须配置
jdbc-type-for-null: null
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 数据库类型,默认值为未知的数据库类型
logic-delete-value: close #逻辑删除
logic-not-delete-value: open # 逻辑打开
banner: false
# ================== ?????????? ==================
management:
security:
enabled: false
# ===================================================
# ================== mvc config ==================
mvc:
static-path-pattern: /**
# ===================================================
# =================== project info ================
project:
info:
name:
server:
port: ${server.port}
# ===================================================
在这个里面,配置了端口等属性,单独把sql的参数抽取出来,方便后期维护
4.4 项目统一父Controoller
/**
* 构造器父类
* @author
*
*/
public class BaseController {
/**
* 返回成功消息
* @param obj 返回空的消息内容
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public HttpFmResult sendSuccessMsg(){
return new HttpFmResult(HttpResultEnums.BUS_ENUM.SUCCESS.KEY, HttpResultEnums.BUS_ENUM.SUCCESS.VALUE,null);
}
/**
* 返回成功消息
* @param obj 消息内容
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public HttpFmResult sendSuccessMsg(Object obj){
return new HttpFmResult(HttpResultEnums.BUS_ENUM.SUCCESS.KEY, HttpResultEnums.BUS_ENUM.SUCCESS.VALUE,obj);
}
/**
* 返回接口调用失败消息
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public HttpFmResult sendFailedMsg(){
return new HttpFmResult(HttpResultEnums.BUS_ENUM.API_ERROR.KEY, HttpResultEnums.BUS_ENUM.API_ERROR.VALUE);
}
/**
* 返回失败消息
* @param key 消息枚举key
* @param obj 消息内容
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public HttpFmResult sendFailedMsg(String key, Object obj){
return new HttpFmResult(key, HttpResultEnums.BUS_ENUM.get(key).VALUE,obj);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public HttpFmResult sendFailedMsg(String key){
return new HttpFmResult(key, HttpResultEnums.BUS_ENUM.get(key).VALUE,null);
}
}
4.5 项目统一返回结果
@ApiModel(value="公共输出对象",description="公共输出对象")
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@ToString
public class HttpFmResult<T>{
@ApiModelProperty(value="输出编码,如:0000",name="code",example="0000")
private String code;
@ApiModelProperty(value="输出消息(String)",name="msg",example="操作成功")
private String msg;
@ApiModelProperty(value="输出对象(Object)",name="data")
private T data;
public HttpFmResult(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public HttpFmResult(HttpResultEnums.BUS_ENUM bus_enum, T data) {
this.code = bus_enum.KEY;
this.msg = bus_enum.VALUE;
this.data = data;
}
public HttpFmResult(String code, String msg) {
this.code = code;
this.msg = msg;
}
}
4.6 全局主键生成器-UUID生成
public class UuidGenderUtils {
/**
* 获得指定数目的UUID
* @param number int 需要获得的UUID数量
* @return String[] UUID数组
*/
public static String[] getUUID(int number){
if(number < 1){
return null;
}
String[] retArray = new String[number];
for(int i=0;i<number;i++){
retArray[i] = getUUID();
}
return retArray;
}
/**
* 获取系统用户id
* @return
*/
public static String getUUID(){
String uuid = UUID.randomUUID().toString();
return uuid.replaceAll("-", "");
}
}
这里至于为什么要使用UUID,就不说了
4.7 解决跨域问题
@Configuration
public class ClassCorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration =new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1
corsConfiguration.addAllowedHeader("*"); // 2
corsConfiguration.addAllowedMethod("*"); // 3
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
// UrlBasedCorsConfigurationSource source= new UrlBasedCorsConfigurationSource();
// source.registerCorsConfiguration("/**", buildConfig()); // 4
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true);
//允许cookies跨域
config.addAllowedOrigin("*");
// 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。
config.addAllowedHeader("*");
// 允许访问的头信息,*表示全部 config.setMaxAge(18000L);
//预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.addAllowedMethod("*");
//允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
// 允许Get的请求方法
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**",config);
return new CorsFilter(source);
}
}
记住也把下面的类帖进去
/**
* spring boot 方式--全局
* 我认为比较优雅的解决方案
* 针对对某个Controller类或者方法可使用@CrossOrigin注解
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET,POST,PUT,DELETE,HEAD,OPTIONS")
.allowedHeaders("*")
.allowCredentials(false).maxAge(3600);
}
}
4.8 解决静态资源映射问题
@Configuration
public class ServletContextConfig extends WebMvcConfigurationSupport {
/**
* 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。
* 需要重新指定静态资源
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
/**
* 配置servlet处理
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
4.9 swagger配置
@EnableSwagger2
@Configuration
public class Swagger2Config implements WebMvcConfigurer {
/**
* 添加资源文件映射
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
}
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("接口稳定")
.description("")
.contact("cxl")
.version("1.0").build();
}
}
4.10 项目统一返回信息枚举
public class HttpResultEnums {
/**
* 业务代码枚举
* @author
*
*/
public static enum BUS_ENUM {
SUCCESS("0000", "success"),
LACK_PARAMETER("0001","缺少必要参数"),
INTERFACE_REEOR("9000","外部接口请求错误"),
DB_ERROR("9001","数据库异常"),
NET_ERROR("9002","内部网络异常"),
API_ERROR("9003","外部接口请求错误"),
//文件相关
FILE_NOTFONUD_ERROR("9004","文件未找到"),
FILE_TYPE_ERROR("9005","文件格式错误"),
//系统错误相关
SYSTEM_ERROR("9999","内部系统错误"),
LOGIN_ERROR("4001","用户账户密码输出错误")
;
public String KEY;
public String VALUE;
private BUS_ENUM(String key, String value) {
this.KEY = key;
this.VALUE = value;
}
public static BUS_ENUM get(String key) {
BUS_ENUM[] values = BUS_ENUM.values();
for (BUS_ENUM object : values) {
if (object.KEY == key) {
return object;
}
}
return null;
}
}
}
这里可以自定义,这里说明下,为什么要用大写,因为在调用的时候,实现了大小写统一,如果自己有强迫症,自己改成小写
4.11 mybatis 慢查询切面
@Aspect
@Component
@Slf4j
public class MapperAspect {
@AfterReturning("execution(* com.cxl.fm.business.*.*.*Dao.*(..))")
public void logServiceAccess(JoinPoint joinPoint) {
log.info("Completed: " + joinPoint);
}
/**
* 监控com.cxl.fm.business..*Mapper包及其子包的所有public方法
*/
@Pointcut("execution(* com.cxl.fm.business.*.*.*Mapper.*(..))")
private void pointCutMethod() {
}
/**
* 声明环绕通知
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("pointCutMethod()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.nanoTime();
Object obj = pjp.proceed();
long end = System.nanoTime();
boolean slowFlag = false;
String searchDesc = "正常";
if(((end - begin) / 1000000) > 100) {
slowFlag = true;
searchDesc = "慢查询";
}
String logData =
""
+ " \n ==> 调用Mapper方法:["+ pjp.getSignature().toString()+"]"
+ " \n ==> 参数:["+ JSON.toJSONString(Arrays.toString(pjp.getArgs()))+"]"
+ " \n ==> 结果:["+ JSON.toJSONString(obj)+"] "
+ " \n ==> 耗时:["+ ((end - begin) / 1000000)+"]毫秒"
+ " \n ==> 运行状态:["+searchDesc+"]"+
"";
System.out.println(logData);
// 插入到日志表中(分慢查询和快查询)
return obj;
}
}
项目结构如下: