欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

项目架构经验之谈之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;
    }
}

项目结构如下:
项目架构经验之谈之SpringBoot单体应用架构分享(一)-项目框架篇

相关标签: springboot