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

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;
    }
}

项目结构如下:
在这里插入图片描述

发布了240 篇原创文章 · 获赞 66 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/u014131617/article/details/103872332