今天在园子里面看到一位大神写的springboot做的框架,感觉挺不错,遂想起来自己还没有一个属于自己的框架,决定先将大神做好的拿过来,然后加入自己觉得需要的模块,不断完善

目前直接复制粘贴过来的,后续要根据自己实际操作中的不同,进行对应的一些修改,包括下面博客中使用的截图以及命名等,还要记录下框架搭建过程中遇到的诸多问题

原文链接:http://www.cnblogs.com/chiangchou/

Spring的简史

第一阶段:XML配置,在Spring1.x时代,使用Spring开发满眼都是xml配置的Bean,随着项目的扩大,我们需要把xml配置文件分放到不同的配置文件里,那时候需要频繁的在开发的类和配置文件之间切换。

第二阶段:注解配置,在Spring2.x时代,Spring提供声明Bean的注解,大大减少了配置量。应用的基本配置用xml,业务配置用注解。

第三阶段:Java配置,从Spring3.x到现在,Spring提供了Java配置,使用Java配置可以让你更理解你所配置的Bean。

Spring Boot:使用“习惯优于配置”的理念让你的项目快速运行起来。使用Spring Boot很容易创建一个独立运行、准生产级别的基于Spring框架的项目,使用Spring Boot你可以不用或者只需要很少的Spring配置。

下面就来使用Spring Boot一步步搭建一个前后端分离的应用开发框架,并且以后不断的去完善这个框架,往里面添加功能。后面以实战为主,不会介绍太多概念,取而代之的是详细的操作。

零、开发技术简介

开发平台:windows

开发工具:Intellij IDEA 2017.2

JDK:Java 8

Maven:maven-3.3.3

服务器:tomcat 8.0

数据库:MySQL 5.7

数据源:Druid1.1.6

缓存:Redis 3.2

日志框架:SLF4J+Logback

Spring Boot:2.0.0RELEASE

ORM框架:MyBatis+通用Mapper

Spring Boot官方文档:Spring Boot Reference Guide

一、创建项目

1、创建项目

这一步直接根据spring的项目构建页面,很方便的生成springboot项目,并且可以自由勾选自己需要的模块

页面链接:Spring Boot

选择页面最下方 Switch to the full version 可以自由搭配自己需要的模块,构建之后这些模块需要的maven配置会自动添加到pom文件里面

2、启动项目

将上一步构建出来的项目下载到本地,然后解压后,使用 IDEA 打开可以看到如下构造:

logback.xml 可以随便去网上拉一个过来用,然后改一下自己需要的配置

pom.xml文件内容如下:

里面包含可能会用到的模块的maven配置,也是根据我们上面初始化项目的时候所勾选的相应的模块

 <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>com.ning</groupId>
<artifactId>framework</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging> <name>framework</name>
<description>myFramework</description> <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties> <dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-solr</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> <repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories> <pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories> </project>

① 在 com.ning.framework 根目录下,新建 core 包,将 FrameworkApplication 先移动到core包下面。这是SpringBoot的入口类,通常是*Application的命名。

入口类里有一个main方法,其实就是一个标准的Java应用的入口方法。在main方法中使用SpringApplication.run启动Spring Boot项目。

然后看看@SpringBootApplication注解,

SpringBootApplication是Spring Boot的核心注解,是一个组合注解。

@EnableAutoConfiguration让Spring Boot根据类路径中的jar包依赖为当前项目进行自动配置。

Spring Boot会自动扫描@SpringBootApplication所在类的同级包以及下级包里的Bean。

② 先启动项目,在 FrameworkApplication 类中直接右击,启动项目,简单观察一下日志 分析一下基本的springboot项目启动的时候都干了些什么事

1.  获取到java的路径,以及一些jar包的地址

2. 打印spring banner这个样式可以自定义,后面会讲到

3. 打印项目启动的时候占用的端口 如line 12 = 6084

4. 打印一个启动时间,然后扫描有没有mybatis的配置文件(line 15),因为我们之间初始化项目的时候有勾选相关模块

5. 显示tomcat的启动端口,这里的6789是我在配置文件【application.properties】中的配置 【server.port=6789】

6. 然后加载一些默认的过滤器

7. 加载 solr

8. 加载freemarker配置 同样因为我们在pom文件引入的相关的配置

9. 启动连接池【HikariPool】,Spring-Boot-2.0.0版本将默认的数据库连接池从tomcat jdbc pool改为了hikari

10. 启动 Quartz 支持

11. 启动 Security 模块

12. 最后就是扫描需要加载的bean和打印启动耗时

 "C:\Program Files\Java\jdk1.8.0_131\bin\java" "-javaagent:D:\IDEA\IntelliJ IDEA 2017.1.1\lib\idea_rt.jar=52621:D:\IDEA\IntelliJ IDEA 2017.1.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;D:\IdeaProjects\framework\target\classes;O:\repository\com\alibaba\fastjson\1.2.24\fastjson-1.2.24.jar;O:\repository\org\apache\commons\commons-lang3\3.5\commons-lang3-3.5.jar;O:\repository\org\springframework\boot\spring-boot-starter-activemq\2.0.0.RELEASE\spring-boot-starter-activemq-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter\2.0.0.RELEASE\spring-boot-starter-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-logging\2.0.0.RELEASE\spring-boot-starter-logging-2.0.0.RELEASE.jar;O:\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;O:\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;O:\repository\org\apache\logging\log4j\log4j-to-slf4j\2.10.0\log4j-to-slf4j-2.10.0.jar;O:\repository\org\apache\logging\log4j\log4j-api\2.10.0\log4j-api-2.10.0.jar;O:\repository\org\slf4j\jul-to-slf4j\1.7.25\jul-to-slf4j-1.7.25.jar;O:\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;O:\repository\org\yaml\snakeyaml\1.19\snakeyaml-1.19.jar;O:\repository\org\springframework\spring-jms\5.0.4.RELEASE\spring-jms-5.0.4.RELEASE.jar;O:\repository\org\springframework\spring-beans\5.0.4.RELEASE\spring-beans-5.0.4.RELEASE.jar;O:\repository\org\apache\activemq\activemq-broker\5.15.3\activemq-broker-5.15.3.jar;O:\repository\org\apache\activemq\activemq-client\5.15.3\activemq-client-5.15.3.jar;O:\repository\org\fusesource\hawtbuf\hawtbuf\1.11\hawtbuf-1.11.jar;O:\repository\org\apache\geronimo\specs\geronimo-j2ee-management_1.1_spec\1.0.1\geronimo-j2ee-management_1.1_spec-1.0.1.jar;O:\repository\org\apache\activemq\activemq-openwire-legacy\5.15.3\activemq-openwire-legacy-5.15.3.jar;O:\repository\com\google\guava\guava\18.0\guava-18.0.jar;O:\repository\com\fasterxml\jackson\core\jackson-databind\2.9.4\jackson-databind-2.9.4.jar;O:\repository\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;O:\repository\com\fasterxml\jackson\core\jackson-core\2.9.4\jackson-core-2.9.4.jar;O:\repository\javax\jms\javax.jms-api\2.0.1\javax.jms-api-2.0.1.jar;O:\repository\org\springframework\boot\spring-boot-starter-batch\2.0.0.RELEASE\spring-boot-starter-batch-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-jdbc\2.0.0.RELEASE\spring-boot-starter-jdbc-2.0.0.RELEASE.jar;O:\repository\com\zaxxer\HikariCP\2.7.8\HikariCP-2.7.8.jar;O:\repository\org\springframework\batch\spring-batch-core\4.0.0.RELEASE\spring-batch-core-4.0.0.RELEASE.jar;O:\repository\javax\batch\javax.batch-api\1.0\javax.batch-api-1.0.jar;O:\repository\org\codehaus\jettison\jettison\1.2\jettison-1.2.jar;O:\repository\org\springframework\batch\spring-batch-infrastructure\4.0.0.RELEASE\spring-batch-infrastructure-4.0.0.RELEASE.jar;O:\repository\org\springframework\retry\spring-retry\1.2.2.RELEASE\spring-retry-1.2.2.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-cache\2.0.0.RELEASE\spring-boot-starter-cache-2.0.0.RELEASE.jar;O:\repository\org\springframework\spring-context\5.0.4.RELEASE\spring-context-5.0.4.RELEASE.jar;O:\repository\org\springframework\spring-expression\5.0.4.RELEASE\spring-expression-5.0.4.RELEASE.jar;O:\repository\org\springframework\spring-context-support\5.0.4.RELEASE\spring-context-support-5.0.4.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-data-redis\2.0.0.RELEASE\spring-boot-starter-data-redis-2.0.0.RELEASE.jar;O:\repository\org\springframework\data\spring-data-redis\2.0.5.RELEASE\spring-data-redis-2.0.5.RELEASE.jar;O:\repository\org\springframework\data\spring-data-keyvalue\2.0.5.RELEASE\spring-data-keyvalue-2.0.5.RELEASE.jar;O:\repository\org\springframework\spring-oxm\5.0.4.RELEASE\spring-oxm-5.0.4.RELEASE.jar;O:\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;O:\repository\io\lettuce\lettuce-core\5.0.2.RELEASE\lettuce-core-5.0.2.RELEASE.jar;O:\repository\io\netty\netty-common\4.1.22.Final\netty-common-4.1.22.Final.jar;O:\repository\io\netty\netty-transport\4.1.22.Final\netty-transport-4.1.22.Final.jar;O:\repository\io\netty\netty-buffer\4.1.22.Final\netty-buffer-4.1.22.Final.jar;O:\repository\io\netty\netty-resolver\4.1.22.Final\netty-resolver-4.1.22.Final.jar;O:\repository\io\netty\netty-handler\4.1.22.Final\netty-handler-4.1.22.Final.jar;O:\repository\io\netty\netty-codec\4.1.22.Final\netty-codec-4.1.22.Final.jar;O:\repository\org\springframework\boot\spring-boot-starter-data-solr\2.0.0.RELEASE\spring-boot-starter-data-solr-2.0.0.RELEASE.jar;O:\repository\org\apache\solr\solr-solrj\6.6.2\solr-solrj-6.6.2.jar;O:\repository\org\apache\commons\commons-math3\3.4.1\commons-math3-3.4.1.jar;O:\repository\org\apache\httpcomponents\httpclient\4.5.5\httpclient-4.5.5.jar;O:\repository\org\apache\httpcomponents\httpcore\4.4.9\httpcore-4.4.9.jar;O:\repository\org\apache\zookeeper\zookeeper\3.4.10\zookeeper-3.4.10.jar;O:\repository\org\codehaus\woodstox\stax2-api\3.1.4\stax2-api-3.1.4.jar;O:\repository\org\codehaus\woodstox\woodstox-core-asl\4.4.1\woodstox-core-asl-4.4.1.jar;O:\repository\org\noggit\noggit\0.6\noggit-0.6.jar;O:\repository\org\springframework\data\spring-data-solr\3.0.5.RELEASE\spring-data-solr-3.0.5.RELEASE.jar;O:\repository\org\springframework\data\spring-data-commons\2.0.5.RELEASE\spring-data-commons-2.0.5.RELEASE.jar;O:\repository\org\apache\httpcomponents\httpmime\4.5.5\httpmime-4.5.5.jar;O:\repository\org\springframework\boot\spring-boot-starter-freemarker\2.0.0.RELEASE\spring-boot-starter-freemarker-2.0.0.RELEASE.jar;O:\repository\org\freemarker\freemarker\2.3.27-incubating\freemarker-2.3.27-incubating.jar;O:\repository\org\springframework\boot\spring-boot-starter-mail\2.0.0.RELEASE\spring-boot-starter-mail-2.0.0.RELEASE.jar;O:\repository\com\sun\mail\javax.mail\1.6.1\javax.mail-1.6.1.jar;O:\repository\javax\activation\activation\1.1\activation-1.1.jar;O:\repository\org\springframework\boot\spring-boot-starter-quartz\2.0.0.RELEASE\spring-boot-starter-quartz-2.0.0.RELEASE.jar;O:\repository\org\springframework\spring-tx\5.0.4.RELEASE\spring-tx-5.0.4.RELEASE.jar;O:\repository\org\quartz-scheduler\quartz\2.3.0\quartz-2.3.0.jar;O:\repository\com\mchange\c3p0\0.9.5.2\c3p0-0.9.5.2.jar;O:\repository\com\mchange\mchange-commons-java\0.2.11\mchange-commons-java-0.2.11.jar;O:\repository\com\zaxxer\HikariCP-java6\2.3.13\HikariCP-java6-2.3.13.jar;O:\repository\org\springframework\boot\spring-boot-starter-security\2.0.0.RELEASE\spring-boot-starter-security-2.0.0.RELEASE.jar;O:\repository\org\springframework\spring-aop\5.0.4.RELEASE\spring-aop-5.0.4.RELEASE.jar;O:\repository\org\springframework\security\spring-security-config\5.0.3.RELEASE\spring-security-config-5.0.3.RELEASE.jar;O:\repository\org\springframework\security\spring-security-web\5.0.3.RELEASE\spring-security-web-5.0.3.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-web\2.0.0.RELEASE\spring-boot-starter-web-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-json\2.0.0.RELEASE\spring-boot-starter-json-2.0.0.RELEASE.jar;O:\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.4\jackson-datatype-jdk8-2.9.4.jar;O:\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.4\jackson-datatype-jsr310-2.9.4.jar;O:\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.4\jackson-module-parameter-names-2.9.4.jar;O:\repository\org\springframework\boot\spring-boot-starter-tomcat\2.0.0.RELEASE\spring-boot-starter-tomcat-2.0.0.RELEASE.jar;O:\repository\org\apache\tomcat\embed\tomcat-embed-core\8.5.28\tomcat-embed-core-8.5.28.jar;O:\repository\org\apache\tomcat\embed\tomcat-embed-el\8.5.28\tomcat-embed-el-8.5.28.jar;O:\repository\org\apache\tomcat\embed\tomcat-embed-websocket\8.5.28\tomcat-embed-websocket-8.5.28.jar;O:\repository\org\hibernate\validator\hibernate-validator\6.0.7.Final\hibernate-validator-6.0.7.Final.jar;O:\repository\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;O:\repository\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;O:\repository\com\fasterxml\classmate\1.3.4\classmate-1.3.4.jar;O:\repository\org\springframework\spring-web\5.0.4.RELEASE\spring-web-5.0.4.RELEASE.jar;O:\repository\org\springframework\spring-webmvc\5.0.4.RELEASE\spring-webmvc-5.0.4.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-webflux\2.0.0.RELEASE\spring-boot-starter-webflux-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-starter-reactor-netty\2.0.0.RELEASE\spring-boot-starter-reactor-netty-2.0.0.RELEASE.jar;O:\repository\io\projectreactor\ipc\reactor-netty\0.7.5.RELEASE\reactor-netty-0.7.5.RELEASE.jar;O:\repository\io\netty\netty-codec-http\4.1.22.Final\netty-codec-http-4.1.22.Final.jar;O:\repository\io\netty\netty-handler-proxy\4.1.22.Final\netty-handler-proxy-4.1.22.Final.jar;O:\repository\io\netty\netty-codec-socks\4.1.22.Final\netty-codec-socks-4.1.22.Final.jar;O:\repository\io\netty\netty-transport-native-epoll\4.1.22.Final\netty-transport-native-epoll-4.1.22.Final.jar;O:\repository\io\netty\netty-transport-native-unix-common\4.1.22.Final\netty-transport-native-unix-common-4.1.22.Final.jar;O:\repository\org\springframework\spring-webflux\5.0.4.RELEASE\spring-webflux-5.0.4.RELEASE.jar;O:\repository\org\synchronoss\cloud\nio-multipart-parser\1.1.0\nio-multipart-parser-1.1.0.jar;O:\repository\org\synchronoss\cloud\nio-stream-storage\1.1.3\nio-stream-storage-1.1.3.jar;O:\repository\org\springframework\boot\spring-boot-starter-websocket\2.0.0.RELEASE\spring-boot-starter-websocket-2.0.0.RELEASE.jar;O:\repository\org\springframework\spring-messaging\5.0.4.RELEASE\spring-messaging-5.0.4.RELEASE.jar;O:\repository\org\springframework\spring-websocket\5.0.4.RELEASE\spring-websocket-5.0.4.RELEASE.jar;O:\repository\org\mybatis\spring\boot\mybatis-spring-boot-starter\1.3.1\mybatis-spring-boot-starter-1.3.1.jar;O:\repository\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\1.3.1\mybatis-spring-boot-autoconfigure-1.3.1.jar;O:\repository\org\mybatis\mybatis\3.4.5\mybatis-3.4.5.jar;O:\repository\org\mybatis\mybatis-spring\1.3.1\mybatis-spring-1.3.1.jar;O:\repository\org\springframework\boot\spring-boot-devtools\2.0.0.RELEASE\spring-boot-devtools-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot\2.0.0.RELEASE\spring-boot-2.0.0.RELEASE.jar;O:\repository\org\springframework\boot\spring-boot-autoconfigure\2.0.0.RELEASE\spring-boot-autoconfigure-2.0.0.RELEASE.jar;O:\repository\mysql\mysql-connector-java\5.1.45\mysql-connector-java-5.1.45.jar;O:\repository\org\springframework\spring-core\5.0.4.RELEASE\spring-core-5.0.4.RELEASE.jar;O:\repository\org\springframework\spring-jcl\5.0.4.RELEASE\spring-jcl-5.0.4.RELEASE.jar;O:\repository\io\projectreactor\reactor-core\3.1.5.RELEASE\reactor-core-3.1.5.RELEASE.jar;O:\repository\org\reactivestreams\reactive-streams\1.0.2\reactive-streams-1.0.2.jar;O:\repository\commons-io\commons-io\2.6\commons-io-2.6.jar;O:\repository\org\springframework\spring-jdbc\5.0.4.RELEASE\spring-jdbc-5.0.4.RELEASE.jar;O:\repository\org\springframework\security\spring-security-core\5.0.3.RELEASE\spring-security-core-5.0.3.RELEASE.jar" com.ning.framework.core.FrameworkApplication
The Class-Path manifest attribute in O:\repository\com\mchange\c3p0\0.9.5.2\c3p0-0.9.5.2.jar referenced one or more files that do not exist: file:/O:/repository/com/mchange/c3p0/0.9.5.2/mchange-commons-java-0.2.11.jar . ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.RELEASE) 2018-03-11 15:31:00,264 [restartedMain] INFO com.ning.framework.core.FrameworkApplication - Starting FrameworkApplication on NING-PC with PID 6084 (D:\IdeaProjects\framework\target\classes started by MR ning in D:\IdeaProjects\framework)
2018-03-11 15:31:00,274 [restartedMain] INFO com.ning.framework.core.FrameworkApplication - No active profile set, falling back to default profiles: default
2018-03-11 15:31:00,374 [restartedMain] INFO o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3d6a9e8b: startup date [Sun Mar 11 15:31:00 CST 2018]; root of context hierarchy
2018-03-11 15:31:01,614 [restartedMain] WARN org.mybatis.spring.mapper.ClassPathMapperScanner - No MyBatis mapper was found in '[com.ning.framework.core]' package. Please check your configuration.
2018-03-11 15:31:01,639 [restartedMain] INFO o.s.d.r.config.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode!
2018-03-11 15:31:01,666 [restartedMain] INFO o.s.d.r.config.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode!
2018-03-11 15:31:02,147 [restartedMain] INFO o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$f369f058] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2018-03-11 15:31:02,727 [restartedMain] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer - Tomcat initialized with port(s): 6789 (http)
2018-03-11 15:31:02,745 [restartedMain] INFO org.apache.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-6789"]
2018-03-11 15:31:02,746 [restartedMain] INFO org.apache.catalina.core.StandardService - Starting service [Tomcat]
2018-03-11 15:31:02,746 [restartedMain] INFO org.apache.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.5.28
2018-03-11 15:31:02,756 [localhost-startStop-1] INFO org.apache.catalina.core.AprLifecycleListener - The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [C:\Program Files\Java\jdk1.8.0_131\bin;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\bin;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\libnvvp;C:\Program Files\Java\jdk1.8.0_131\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;D:\apache-maven-3.3.3\bin;D:\Python\Python36;D:\Python\Python36\Scripts;D:\MyTensorFlow\cuda\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;D:\jump_auto\usb_driver;C:\Users\MR ning\AppData\Local\Microsoft\WindowsApps;;.]
2018-03-11 15:31:02,879 [localhost-startStop-1] INFO o.a.c.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2018-03-11 15:31:02,879 [localhost-startStop-1] INFO org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 2505 ms
2018-03-11 15:31:03,067 [localhost-startStop-1] INFO o.s.boot.web.servlet.FilterRegistrationBean - Mapping filter: 'characterEncodingFilter' to: [/*]
2018-03-11 15:31:03,067 [localhost-startStop-1] INFO o.s.boot.web.servlet.FilterRegistrationBean - Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-03-11 15:31:03,067 [localhost-startStop-1] INFO o.s.boot.web.servlet.FilterRegistrationBean - Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-03-11 15:31:03,067 [localhost-startStop-1] INFO o.s.boot.web.servlet.FilterRegistrationBean - Mapping filter: 'requestContextFilter' to: [/*]
2018-03-11 15:31:03,067 [localhost-startStop-1] INFO o.s.b.w.s.DelegatingFilterProxyRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
2018-03-11 15:31:03,067 [localhost-startStop-1] INFO o.s.boot.web.servlet.ServletRegistrationBean - Servlet dispatcherServlet mapped to [/]
2018-03-11 15:31:03,941 [restartedMain] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3d6a9e8b: startup date [Sun Mar 11 15:31:00 CST 2018]; root of context hierarchy
2018-03-11 15:31:04,033 [restartedMain] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-03-11 15:31:04,035 [restartedMain] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-03-11 15:31:04,082 [restartedMain] INFO o.s.web.servlet.handler.SimpleUrlHandlerMapping - Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-11 15:31:04,082 [restartedMain] INFO o.s.web.servlet.handler.SimpleUrlHandlerMapping - Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-11 15:31:04,130 [restartedMain] INFO o.s.web.servlet.handler.SimpleUrlHandlerMapping - Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-11 15:31:04,407 [restartedMain] WARN org.springframework.data.convert.CustomConversions - Registering converter from interface org.springframework.data.solr.core.query.Update to class org.apache.solr.common.SolrInputDocument as writing converter although it doesn't convert to a store-supported type! You might wanna check you annotation setup at the converter implementation.
2018-03-11 15:31:04,415 [restartedMain] WARN org.springframework.data.convert.CustomConversions - Registering converter from interface org.springframework.data.solr.core.query.Update to class org.apache.solr.common.SolrInputDocument as writing converter although it doesn't convert to a store-supported type! You might wanna check you annotation setup at the converter implementation.
2018-03-11 15:31:04,742 [restartedMain] INFO o.s.ui.freemarker.SpringTemplateLoader - SpringTemplateLoader for FreeMarker: using resource loader [org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3d6a9e8b: startup date [Sun Mar 11 15:31:00 CST 2018]; root of context hierarchy] and template loader path [classpath:/templates/]
2018-03-11 15:31:04,743 [restartedMain] INFO o.s.w.servlet.view.freemarker.FreeMarkerConfigurer - ClassTemplateLoader for Spring macros added to FreeMarker configuration
2018-03-11 15:31:04,762 [restartedMain] WARN o.s.b.a.freemarker.FreeMarkerAutoConfiguration - Cannot find template location(s): [classpath:/templates/] (please add some templates, check your FreeMarker configuration, or set spring.freemarker.checkTemplateLocation=false)
2018-03-11 15:31:04,954 [restartedMain] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2018-03-11 15:31:05,206 [restartedMain] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2018-03-11 15:31:05,257 [restartedMain] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
2018-03-11 15:31:05,274 [restartedMain] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
2018-03-11 15:31:05,274 [restartedMain] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.0 created.
2018-03-11 15:31:05,275 [restartedMain] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
2018-03-11 15:31:05,275 [restartedMain] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.0) 'quartzScheduler' with instanceId 'NON_CLUSTERED'
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 2018-03-11 15:31:05,275 [restartedMain] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance.
2018-03-11 15:31:05,275 [restartedMain] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.0
2018-03-11 15:31:05,276 [restartedMain] INFO org.quartz.core.QuartzScheduler - JobFactory set to: org.springframework.boot.autoconfigure.quartz.AutowireCapableBeanJobFactory@332f68db
2018-03-11 15:31:05,425 [restartedMain] INFO o.s.b.a.s.s.UserDetailsServiceAutoConfiguration - Using generated security password: 45297dde-2147-4379-a060-837232a35e96 2018-03-11 15:31:05,634 [restartedMain] INFO o.s.security.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@2cf77b86, org.springframework.security.web.context.SecurityContextPersistenceFilter@33b4ab19, org.springframework.security.web.header.HeaderWriterFilter@740d3179, org.springframework.security.web.csrf.CsrfFilter@6c7851ed, org.springframework.security.web.authentication.logout.LogoutFilter@3bb3f32c, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@2a457ee3, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@463699c2, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@33118e85, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@54d9d215, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@35463d2e, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@18af4e4a, org.springframework.security.web.session.SessionManagementFilter@140b6aa, org.springframework.security.web.access.ExceptionTranslationFilter@47fe504a, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@a6d6263]
2018-03-11 15:31:05,793 [restartedMain] INFO o.s.b.d.autoconfigure.OptionalLiveReloadServer - LiveReload server is running on port 35729
2018-03-11 15:31:05,857 [restartedMain] INFO o.s.jmx.export.annotation.AnnotationMBeanExporter - Registering beans for JMX exposure on startup
2018-03-11 15:31:05,859 [restartedMain] INFO o.s.jmx.export.annotation.AnnotationMBeanExporter - Bean with name 'dataSource' has been autodetected for JMX exposure
2018-03-11 15:31:05,867 [restartedMain] INFO o.s.jmx.export.annotation.AnnotationMBeanExporter - Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2018-03-11 15:31:05,873 [restartedMain] INFO o.s.context.support.DefaultLifecycleProcessor - Starting beans in phase 2147483647
2018-03-11 15:31:05,873 [restartedMain] INFO o.s.scheduling.quartz.SchedulerFactoryBean - Starting Quartz Scheduler now
2018-03-11 15:31:05,874 [restartedMain] INFO org.quartz.core.QuartzScheduler - Scheduler quartzScheduler_$_NON_CLUSTERED started.
2018-03-11 15:31:05,880 [restartedMain] INFO org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-6789"]
2018-03-11 15:31:05,889 [restartedMain] INFO org.apache.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
2018-03-11 15:31:05,920 [restartedMain] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer - Tomcat started on port(s): 6789 (http) with context path ''
2018-03-11 15:31:05,924 [restartedMain] INFO com.ning.framework.core.FrameworkApplication - Started FrameworkApplication in 6.14 seconds (JVM running for 7.143)

③ 替换默认的banner

可以到http://patorjk.com/software/taag/这个网站生成一个自己项目的banner。创建banner.txt并放到resources根目录下。

${AnsiColor.YELLOW}
__ ____ __ _____ ____ _ __ __ _______ _____ ____ _ __
| \/ \ \ / / | ___| _ \ / \ | \/ | ____\ \ / / _ \| _ \| |/ /
| |\/| |\ V / | |_ | |_) | / _ \ | |\/| | _| \ \ /\ / | | | | |_) | ' /
| | | | | | | _| | _ < / ___ \| | | | |___ \ V V /| |_| | _ <| . \
|_| |_| |_| |_| |_| \_/_/ \_|_| |_|_____| \_/\_/ \___/|_| \_|_|\_\
============================================================================
spring-boot version: ${spring-boot.formatted-version}

启动后显示如下:

4、Spring Boot 配置

① 配置文件

Spring Boot使用一个全局的配置文件application.properties或application.yaml,放置在src/main/resources目录下。我们可以在这个全局配置文件中对一些默认的配置值进行修改。

具体有哪些配置可到官网查找,有非常多的配置,不过大部分使用默认即可。Common application properties

然后,需要为不同的环境配置不同的配置文件,全局使用application-{profile}.properties指定不同环境配置文件。

我这里增加了开发环境(dev)和生产环境(prod)的配置文件,并通过在application.properties中设置spring.profiles.active=dev来指定当前环境。

② starter pom

Spring Boot为我们提供了简化开发绝大多数场景的starter pom,只要使用了应用场景所需的starter pom,无需繁杂的配置,就可以得到Spring Boot为我们提供的自动配置的Bean。

后面我们将会通过加入这些starter来一步步集成我们想要的功能。具体有哪些starter,可以到官网查看:Starters

③ 自动配置

Spring Boot关于自动配置的源码在spring-boot-autoconfigure中如下:

我们可以在application.properties中加入debug=true,查看当前项目中已启用和未启用的自动配置。

我们在application.properties中的配置其实就是覆盖spring-boot-autoconfigure里的默认配置,比如web相关配置在web包下。

常见的如HttpEncodingProperties配置http编码,里面自动配置的编码为UTF-8。

MultipartProperties,上传文件的属性,设置了上传最大文件1M。

ServerProperties,配置内嵌Servlet容器,配置端口、contextPath等等。

之前说@SpringBootApplication是Spring Boot的核心注解,但他的核心功能是由@EnableAutoConfiguration注解提供的。

@EnableAutoConfiguration注解通过@Import导入配置功能,在AutoConfigurationImportSelector中,通过SpringFactoriesLoader.loadFactoryNames扫描META-INF/spring.factories文件。

在spring.factories中,配置了需要自动配置的类,我们也可以通过这种方式添加自己的自动配置。

在spring-boot-autoconfigure下就有一个spring.factories,如下:

说了这么多,只为说明一点,Spring Boot为我们做了很多自动化的配置,搭建快速方便。

但是,正因为它为我们做了很多事情,就有很多坑,有时候,出了问题,我们可能很难找出问题所在,这时候,我们可能就要考虑下是否是自动配置导致的,有可能配置冲突了,或者没有使用上自定义的配置等等。

5、项目结构划分

自修改到此,以上内容已经修改为我自己的环境,将原有的图片以及一些配置修改,下面的后面会继续完善

core是项目的核心模块,结构初步规划如下:

 base是项目的基础核心,定义一些基础类,如BaseController、BaseService等;

cache是缓存相关;

config是配置中心,模块所有的配置放到config里统一管理;

constants里定义系统的常量。

exception里封装一些基础的异常类;

 system是系统模块;

util里则是一些通用工具类;

二、基础结构功能

1、web支持

只需在pom.xml中加入spring-boot-starter-web的依赖即可。

之后,查看POM的依赖树(插件:Maven Helper),可以看到引入了starter、tomcat、web支持等。可以看出,Sping Boot内嵌了servlet容器,默认tomcat。

自动配置在WebMvcAutoConfiguration和WebMvcProperties里,可自行查看源码,一般我们不需添加其他配置就可以启动这个web项目了。

2、基础功能

在core中添加一些基础的功能支持。

① 首先引入一些常用的依赖库,主要是一些常用工具类,方便以后的开发。

<!-- ******************************* 常用依赖库 ********************************** -->
<!-- 针对开发IO流功能的工具类库 -->
<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>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 常用的集合操作,丰富的工具类 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons.collections.version}</version>
</dependency>
<!-- 操作javabean的工具包 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${commons.beanutils.version}</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 包含一些通用的编码解码算法. 如:MD5、SHA1、Base64等 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons.codec.version}</version>
</dependency>
<!-- 包含丰富的工具类如 StringUtils -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<!--
Guava工程包含了若干被Google的Java项目广泛依赖的核心库. 集合[collections] 、缓存[caching] 、原生类型支持[primitives support] 、
并发库[concurrency libraries] 、通用注解[common annotations] 、字符串处理[string processing] 、I/O 等等。
-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>

版本号如下:

② 在base添加一个Result类,作为前端的返回对象,Controller的直接返回对象都是Result。

package com.lyyzoo.core.base;

import com.fasterxml.jackson.annotation.JsonInclude;

import java.io.Serializable;

/**
* 前端返回对象
*
* @version 1.0
* @author bojiangzhou 2017-12-28
*/
public class Result implements Serializable {
private static final long serialVersionUID = 1430633339880116031L;

/**
* 成功与否标志
*/
private boolean success = true;
/**
* 返回状态码,为空则默认200.前端需要拦截一些常见的状态码如403、404、500等
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private Integer status;
/**
* 编码,可用于前端处理多语言,不需要则不用返回编码
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String code;
/**
* 相关消息
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String msg;
/**
* 相关数据
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private Object data;

public Result() {}

public Result(boolean success) {
this.success = success;
}

public Result(boolean success, Integer status) {
this.success = success;
this.status = status;
}

public Result(boolean success, String code, String msg){
this(success);
this.code = code;
this.msg = msg;
}

public Result(boolean success, Integer status, String code, String msg) {
this.success = success;
this.status = status;
this.code = code;
this.msg = msg;
}

public Result(boolean success, String code, String msg, Object data){
this(success);
this.code = code;
this.msg = msg;
this.data = data;
}

public boolean isSuccess() {
return success;
}

public void setSuccess(boolean success) {
this.success = success;
}

public Integer getStatus() {
return status;
}

public void setStatus(Integer status) {
this.status = status;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}
}

之后在util添加生成Result的工具类Results,用于快速方便的创建Result对象。

package com.lyyzoo.core.util;

import com.lyyzoo.core.base.Result;

/**
* Result生成工具类
*
* @version 1.0
* @author bojiangzhou 2017-12-28
*/
public class Results {

protected Results() {}

public static Result newResult() {
return new Result();

}

public static Result newResult(boolean success) {
return new Result(success);
}

//
// 业务调用成功
// ----------------------------------------------------------------------------------------------------
public static Result success() {
return new Result();
}

public static Result success(String msg) {
return new Result(true, null, msg);
}

public static Result success(String code, String msg) {
return new Result(true, code, msg);
}

public static Result successWithStatus(Integer status) {
return new Result(true, status);
}

public static Result successWithStatus(Integer status, String msg) {
return new Result(true, status, null, msg);
}

public static Result successWithData(Object data) {
return new Result(true, null, null, data);
}

public static Result successWithData(Object data, String msg) {
return new Result(true, null, msg, data);
}

public static Result successWithData(Object data, String code, String msg) {
return new Result(true, code, msg, data);
}

//
// 业务调用失败
// ----------------------------------------------------------------------------------------------------
public static Result failure() {
return new Result(false);
}

public static Result failure(String msg) {
return new Result(false, null, msg);
}

public static Result failure(String code, String msg) {
return new Result(false, code, msg);
}

public static Result failureWithStatus(Integer status) {
return new Result(false, status);
}

public static Result failureWithStatus(Integer status, String msg) {
return new Result(false, status, null, msg);
}

public static Result failureWithData(Object data) {
return new Result(false, null, null, data);
}

public static Result failureWithData(Object data, String msg) {
return new Result(false, null, msg, data);
}

public static Result failureWithData(Object data, String code, String msg) {
return new Result(false, code, msg, data);
}

}

③ 在base添加BaseEnum<K, V>枚举接口,定义了获取值和描述的接口。

package com.lyyzoo.core.base;

/**
* 基础枚举接口
*
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
public interface BaseEnum<K, V> {

/**
* 获取编码
*
* @return 编码
*/
K code();

/**
* 获取描述
*
* @return 描述
*/
V desc();

}

然后在constants下定义一个基础枚举常量类,我们把一些描述信息维护到枚举里面,尽量不要在代码中直接出现魔法值(如一些编码、中文等),以后的枚举常量类也可以按照这种模式来写。

package com.lyyzoo.core.constants;

import com.lyyzoo.core.base.BaseEnum;

import java.util.HashMap;
import java.util.Map;

/**
* 基础枚举值
*
* @version 1.0
* @author bojiangzhou 2018-01-01
*/
public enum BaseEnums implements BaseEnum<String, String> {

SUCCESS("request.success", "请求成功"),

FAILURE("request.failure", "请求失败"),

OPERATION_SUCCESS("operation.success", "操作成功"),

OPERATION_FAILURE("operation.failure", "操作失败"),

ERROR("system.error", "系统异常"),

NOT_FOUND("not_found", "请求资源不存在"),

FORBIDDEN("forbidden", "无权限访问"),

VERSION_NOT_MATCH("record_not_exists_or_version_not_match", "记录版本不存在或不匹配"),

PARAMETER_NOT_NULL("parameter_not_be_null", "参数不能为空");

private String code;

private String desc;

private static Map<String, String> allMap = new HashMap<>();

BaseEnums(String code, String desc) {
this.code = code;
this.desc = desc;
}

static {
for(BaseEnums enums : BaseEnums.values()){
allMap.put(enums.code, enums.desc);
}
}

@Override
public String code() {
return code;
}

@Override
public String desc() {
return desc;
}

public String desc(String code) {
return allMap.get(code);
}

}

④ 再添加一个常用的日期工具类对象,主要包含一些常用的日期时间格式化,后续可再继续往里面添加一些公共方法。

package com.lyyzoo.core.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 日期时间工具类
*
* @version 1.0
* @author bojiangzhou 2017-12-28
*/
public class Dates {

/**
* 日期时间匹配格式
*/
public interface Pattern {
//
// 常规模式
// ----------------------------------------------------------------------------------------------------
/**
* yyyy-MM-dd
*/
String DATE = "yyyy-MM-dd";
/**
* yyyy-MM-dd HH:mm:ss
*/
String DATETIME = "yyyy-MM-dd HH:mm:ss";
/**
* yyyy-MM-dd HH:mm
*/
String DATETIME_MM = "yyyy-MM-dd HH:mm";
/**
* yyyy-MM-dd HH:mm:ss.SSS
*/
String DATETIME_SSS = "yyyy-MM-dd HH:mm:ss.SSS";
/**
* HH:mm
*/
String TIME = "HH:mm";
/**
* HH:mm:ss
*/
String TIME_SS = "HH:mm:ss";

//
// 系统时间格式
// ----------------------------------------------------------------------------------------------------
/**
* yyyy/MM/dd
*/
String SYS_DATE = "yyyy/MM/dd";
/**
* yyyy/MM/dd HH:mm:ss
*/
String SYS_DATETIME = "yyyy/MM/dd HH:mm:ss";
/**
* yyyy/MM/dd HH:mm
*/
String SYS_DATETIME_MM = "yyyy/MM/dd HH:mm";
/**
* yyyy/MM/dd HH:mm:ss.SSS
*/
String SYS_DATETIME_SSS = "yyyy/MM/dd HH:mm:ss.SSS";

//
// 无连接符模式
// ----------------------------------------------------------------------------------------------------
/**
* yyyyMMdd
*/
String NONE_DATE = "yyyyMMdd";
/**
* yyyyMMddHHmmss
*/
String NONE_DATETIME = "yyyyMMddHHmmss";
/**
* yyyyMMddHHmm
*/
String NONE_DATETIME_MM = "yyyyMMddHHmm";
/**
* yyyyMMddHHmmssSSS
*/
String NONE_DATETIME_SSS = "yyyyMMddHHmmssSSS";
}

public static final String DEFAULT_PATTERN = Pattern.DATETIME;

public static final String[] PARSE_PATTERNS = new String[]{
Pattern.DATE,
Pattern.DATETIME,
Pattern.DATETIME_MM,
Pattern.DATETIME_SSS,
Pattern.SYS_DATE,
Pattern.SYS_DATETIME,
Pattern.SYS_DATETIME_MM,
Pattern.SYS_DATETIME_SSS
};

/**
* 格式化日期时间
*
* @param date 日期时间
*
* @return yyyy-MM-dd HH:mm:ss
*/
public static String format(Date date) {
return format(date, DEFAULT_PATTERN);
}

/**
* 格式化日期
*
* @param date 日期(时间)
*
* @param pattern 匹配模式 参考:{@link Dates.Pattern}
*
* @return 格式化后的字符串
*/
public static String format(Date date, String pattern) {
if (date == null) {
return null;
}
pattern = StringUtils.isNotBlank(pattern) ? pattern : DEFAULT_PATTERN;
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.format(date);
}

/**
* 解析日期
*
* @param date 日期字符串
*
* @return 解析后的日期 默认格式:yyyy-MM-dd HH:mm:ss
*/
public static Date parseDate(String date) {
if (StringUtils.isBlank(date)) {
return null;
}
try {
return DateUtils.parseDate(date, PARSE_PATTERNS);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}

/**
* 解析日期
*
* @param date 日期
*
* @param pattern 格式 参考:{@link Dates.Pattern}
*
* @return 解析后的日期,默认格式:yyyy-MM-dd HH:mm:ss
*/
public static Date parseDate(String date, String pattern) {
if (StringUtils.isBlank(date)) {
return null;
}
String[] parsePatterns;
parsePatterns = StringUtils.isNotBlank(pattern) ? new String[]{pattern} : PARSE_PATTERNS;
try {
return DateUtils.parseDate(date, parsePatterns);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}

}

⑤ Constants定义系统级的通用常量。

package com.lyyzoo.core.constants;

import com.google.common.base.Charsets;

import java.nio.charset.Charset;

/**
* 系统级常量类
*
* @version 1.0
* @author bojiangzhou 2017-12-28
*/
public class Constants {

public static final String APP_NAME = "sunny";

/**
* 系统编码
*/
public static final Charset CHARSET = Charsets.UTF_8;

/**
* 标识:是/否、启用/禁用等
*/
public interface Flag {

Integer YES = 1;

Integer NO = 0;
}

/**
* 操作类型
*/
public interface Operation {
/**
* 添加
*/
String ADD = "add";
/**
* 更新
*/
String UPDATE = "update";
/**
* 删除
*/
String DELETE = "delete";
}

/**
* 性别
*/
public interface Sex {
/**
* 男
*/
Integer MALE = 1;
/**
* 女
*/
Integer FEMALE = 0;
}

}

⑥ 在base添加空的BaseController、BaseDTO、Service、Mapper,先定义好基础结构,后面再添加功能。

BaseDTO:标准的who字段、版本号、及10个扩展字段。

因为这里用到了@Transient注解,先引入java持久化包:

package com.lyyzoo.core.base;

import com.fasterxml.jackson.annotation.*;
import com.lyyzoo.core.Constants;
import com.lyyzoo.core.util.Dates;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import javax.persistence.Transient;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* 基础实体类
*
* @version 1.0
* @author bojiangzhou 2017-12-29
*/
public class BaseDTO implements Serializable {
private static final long serialVersionUID = -4287607489867805101L;

public static final String FIELD_OPERATE = "operate";
public static final String FIELD_OBJECT_VERSION_NUMBER = "versionNumber";
public static final String FIELD_CREATE_BY = "createBy";
public static final String FIELD_CREATOR = "creator";
public static final String FIELD_CREATE_DATE = "createDate";
public static final String FIELD_UPDATE_BY = "updateBy";
public static final String FIELD_UPDATER = "updater";
public static final String FIELD_UPDATE_DATE = "updateDate";

/**
* 操作类型,add/update/delete 参考:{@link Constants.Operation}
*/
@Transient
private String _operate;

/**
* 数据版本号,每发生update则自增,用于实现乐观锁.
*/
private Long versionNumber;

//
// 下面是标准 WHO 字段
// ----------------------------------------------------------------------------------------------------
/**
* 创建人用户名
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long createBy;
/**
* 创建人名称
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Transient
private String creator;
/**
* 创建时间
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonFormat(pattern = Dates.DEFAULT_PATTERN)
private Date createDate;

/**
* 更新人用户名
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long updateBy;
/**
* 更新人名称
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Transient
private String updater;
/**
* 更新时间
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonFormat(pattern = Dates.DEFAULT_PATTERN)
private Date updateDate;

/**
* 其它属性
*/
@JsonIgnore
@Transient
protected Map<String, Object> innerMap = new HashMap<>();

//
// 下面是扩展属性字段
// ----------------------------------------------------------------------------------------------------

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute1;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute2;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute3;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute4;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute5;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute6;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute7;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute8;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute9;

@JsonInclude(JsonInclude.Include.NON_NULL)
private String attribute10;

public String get_operate() {
return _operate;
}

public void set_operate(String _operate) {
this._operate = _operate;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}

public String toJSONString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
}

public Long getVersionNumber() {
return versionNumber;
}

public void setVersionNumber(Long versionNumber) {
this.versionNumber = versionNumber;
}

public Long getCreateBy() {
return createBy;
}

public void setCreateBy(Long createBy) {
this.createBy = createBy;
}

public String getCreator() {
return creator;
}

public void setCreator(String creator) {
this.creator = creator;
}

public Date getCreateDate() {
return createDate;
}

public void setCreateDate(Date createDate) {
this.createDate = createDate;
}

public Long getUpdateBy() {
return updateBy;
}

public void setUpdateBy(Long updateBy) {
this.updateBy = updateBy;
}

public String getUpdater() {
return updater;
}

public void setUpdater(String updater) {
this.updater = updater;
}

public Date getUpdateDate() {
return updateDate;
}

public void setUpdateDate(Date updateDate) {
this.updateDate = updateDate;
}

@JsonAnyGetter
public Object getAttribute(String key) {
return innerMap.get(key);
}

@JsonAnySetter
public void setAttribute(String key, Object obj) {
innerMap.put(key, obj);
}

public String getAttribute1() {
return attribute1;
}

public void setAttribute1(String attribute1) {
this.attribute1 = attribute1;
}

public String getAttribute2() {
return attribute2;
}

public void setAttribute2(String attribute2) {
this.attribute2 = attribute2;
}

public String getAttribute3() {
return attribute3;
}

public void setAttribute3(String attribute3) {
this.attribute3 = attribute3;
}

public String getAttribute4() {
return attribute4;
}

public void setAttribute4(String attribute4) {
this.attribute4 = attribute4;
}

public String getAttribute5() {
return attribute5;
}

public void setAttribute5(String attribute5) {
this.attribute5 = attribute5;
}

public String getAttribute6() {
return attribute6;
}

public void setAttribute6(String attribute6) {
this.attribute6 = attribute6;
}

public String getAttribute7() {
return attribute7;
}

public void setAttribute7(String attribute7) {
this.attribute7 = attribute7;
}

public String getAttribute8() {
return attribute8;
}

public void setAttribute8(String attribute8) {
this.attribute8 = attribute8;
}

public String getAttribute9() {
return attribute9;
}

public void setAttribute9(String attribute9) {
this.attribute9 = attribute9;
}

public String getAttribute10() {
return attribute10;
}

public void setAttribute10(String attribute10) {
this.attribute10 = attribute10;
}

}

同时,重写了toString方法,增加了toJsonString方法,使得可以格式化输出DTO的数据:

直接打印DTO,输出的格式大概就是这个样子:

⑦ 在exception添加BaseException,定义一些基础异常类

基础异常类都继承自运行时异常类(RunntimeException),尽可能把受检异常转化为非受检异常,更好的面向接口编程,提高代码的扩展性、稳定性。

BaseException:添加了一个错误编码,其它自定义的异常应当继承该类。

package com.lyyzoo.core.exception;

/**
* 基础异常类
*
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
public class BaseException extends RuntimeException {
private static final long serialVersionUID = -997101946070796354L;

/**
* 错误编码
*/
protected String code;

public BaseException() {}

public BaseException(String message) {
super(message);
}

public BaseException(String code, String message) {
super(message);
this.code = code;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}
}

ServiceException:继承BaseException,Service层往Controller抛出的异常。

package com.lyyzoo.core.exception;

/**
* Service层异常
*
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
public class ServiceException extends BaseException {
private static final long serialVersionUID = 6058294324031642376L;

public ServiceException() {}

public ServiceException(String message) {
super(message);
}

public ServiceException(String code, String message) {
super(code, message);
}

}

3、添加系统用户功能,使用Postman测试接口

① 在system模块下,再分成dto、controller、service、mapper、constants子包,以后一个模块功能开发就是这样一个基础结构。

User:系统用户

package com.lyyzoo.core.system.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lyyzoo.core.base.BaseDTO;
import com.lyyzoo.core.util.Dates;

import java.util.Date;

/**
* 系统用户
*
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User extends BaseDTO {
private static final long serialVersionUID = -7395431342743009038L;

/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 生日
*/
@JsonFormat(pattern = Dates.Pattern.DATE)
private Date birthday;
/**
* 性别:1-男/0-女
*/
private Integer sex;
/**
* 是否启用:1/0
*/
private Integer enabled;

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

public Integer getSex() {
return sex;
}

public void setSex(Integer sex) {
this.sex = sex;
}

public Integer getEnabled() {
return enabled;
}

public void setEnabled(Integer enabled) {
this.enabled = enabled;
}

}

UserController:用户控制层;用@RestController注解,前后端分离,因为无需返回视图,采用Restful风格,直接返回数据。

package com.lyyzoo.core.system.controller;

import com.lyyzoo.core.Constants;
import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.BaseEnums;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.util.Dates;
import com.lyyzoo.core.util.Results;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
* 用户Controller
*
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
@RequestMapping("/sys/user")
@RestController
public class UserController extends BaseController {

private static List<User> userList = new ArrayList<>();

// 先静态模拟数据
static {
User user1 = new User();
user1.setUserId(1L);
user1.setUsername("lufei");
user1.setNickname("蒙奇D路飞");
user1.setBirthday(Dates.parseDate("2000-05-05"));
user1.setSex(Constants.Sex.MALE);
user1.setEnabled(Constants.Flag.YES);
userList.add(user1);

User user2 = new User();
user2.setUserId(2L);
user2.setUsername("nami");
user2.setNickname("娜美");
user2.setBirthday(Dates.parseDate("2000/7/3"));
user2.setSex(Constants.Sex.FEMALE);
user2.setEnabled(Constants.Flag.YES);
userList.add(user2);
}

@RequestMapping("/queryAll")
public Result queryAll(){
return Results.successWithData(userList, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
}

@RequestMapping("/queryOne/{userId}")
public Result queryOne(@PathVariable Long userId){
User user = null;
for(User u : userList){
if(u.getUserId().longValue() == userId){
user = u;
}
}
return Results.successWithData(user);
}
}

② Postman请求:请求成功,基础的HTTP服务已经实现了。

三、集成MyBatis,实现基础Mapper和Service

1、添加JDBC、配置数据源

添加spring-boot-starter-jdbc以支持JDBC访问数据库,然后添加MySql的JDBC驱动mysql-connector-java;

在application.properties里配置mysql的数据库驱动

之后在application-dev.properties里配置开发环境数据库的连接信息,添加之后,Springboot就会自动配置数据源了。

2、集成MyBatis

MyBatis官方为了方便Springboot集成MyBatis,专门提供了一个符合Springboot规范的starter项目,即mybatis-spring-boot-starter。

在application.properties里添加mybatis映射配置:

3、添加MyBatis通用Mapper

通用Mapper可以极大的简化开发,极其方便的进行单表的增删改查。

关于通用Mapper,参考网站地址:

  MyBatis通用Mapper

  MyBatis 相关工具

之后,在core.base下创建自定义的Mapper,按需选择接口。

具体可参考:根据需要自定义接口

package com.lyyzoo.core.base;

import tk.mybatis.mapper.common.BaseMapper;
import tk.mybatis.mapper.common.ConditionMapper;
import tk.mybatis.mapper.common.IdsMapper;
import tk.mybatis.mapper.common.special.InsertListMapper;

/**
*
* BaseMapper
*
* @name BaseMapper
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
public interface Mapper<T> extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> {

}

定义好基础Mapper后,就具有下图中的基本通用方法了。每个实体类对应的*Mapper继承Mapper<T>来获得基本的增删改查的通用方法。

在application.properties里配置自定义的基础Mapper

4、添加分页插件PageHelper

参考地址:

  MyBatis 分页插件 - PageHelper

  分页插件使用方法

分页插件配置,一般情况下,不需要做任何配置。

之后,我们就可以在代码中使用 PageHelper.startPage(1, 10) 对紧随其后的一个查询进行分页查询,非常方便。

5、配置自动扫描Mapper

在config下创建MyBatisConfig配置文件,通过mapperScannerConfigurer方法配置自动扫描Mapper文件。

package com.lyyzoo.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import tk.mybatis.spring.mapper.MapperScannerConfigurer;

/**
* MyBatis相关配置.
*
* @version 1.0
* @author bojiangzhou 2018-01-07
*/
@Configuration
public class MyBatisConfig {

/**
* Mapper扫描配置. 自动扫描将Mapper接口生成代理注入到Spring.
*/
@Bean
public static MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
// 注意这里的扫描路径: 1.不要扫描到自定义的Mapper; 2.定义的路径不要扫描到tk.mybatis.mapper(如定义**.mapper).
// 两个做法都会导致扫描到tk.mybatis的Mapper,就会产生重复定义的报错.
mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper");
return mapperScannerConfigurer;
}

}

注意这里的 MapperScannerConfigurer 是tk.mybatis.spring.mapper.MapperScannerConfigurer,而不是org.mybatis,否则使用通用Mapper的方法时会报类似下面的这种错误

6、定义基础Service

一般来说,我们不能在Controller中直接访问Mapper,因此我们需要加上Service,通过Service访问Mapper。

首先定义基础Service<T>接口,根据Mapper定义基本的增删改查接口方法。

package com.lyyzoo.core.base;

import java.util.List;

/**
* Service 基础通用接口
*
* @name BaseService
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
public interface Service<T> {

//
// insert
// ----------------------------------------------------------------------------------------------------
/**
* 保存一个实体,null的属性也会保存,不会使用数据库默认值
*
* @param record
* @return
*/
T insert(T record);

/**
* 批量插入,null的属性也会保存,不会使用数据库默认值
*
* @param recordList
* @return
*/
List<T> insert(List<T> recordList);

/**
* 保存一个实体,null的属性不会保存,会使用数据库默认值
*
* @param record
* @return
*/
T insertSelective(T record);

/**
* 批量插入,null的属性不会保存,会使用数据库默认值
*
* @param recordList
* @return
*/
List<T> insertSelective(List<T> recordList);

//
// update
// ----------------------------------------------------------------------------------------------------
/**
* 根据主键更新实体全部字段,null值会被更新
*
* @param record
* @return
*/
T update(T record);

/**
* 批量更新,根据主键更新实体全部字段,null值会被更新
*
* @param recordList
* @return
*/
List<T> update(List<T> recordList);

/**
* 根据主键更新属性不为null的值
*
* @param record
* @return
*/
T updateSelective(T record);

/**
* 批量更新,根据主键更新属性不为null的值
*
* @param recordList
* @return
*/
List<T> updateSelective(List<T> recordList);

//
// delete
// ----------------------------------------------------------------------------------------------------
/**
* 根据主键删除
*
* @param id id不能为空
* @return
*/
int delete(Long id);

/**
* 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段
*
* @param ids 类似1,2,3
*/
int delete(String ids);

/**
* 根据主键删除多个实体,ID数组
*
* @param ids 类似[1,2,3],不能为空
*/
int delete(Long[] ids);

/**
* 根据实体属性作为条件进行删除
*
* @param record
* @return
*/
int delete(T record);

/**
* 根据主键删除多个实体
*
* @param recordList
* @return
*/
int delete(List<T> recordList);

//
// insert or update or delete
// ----------------------------------------------------------------------------------------------------
/**
* 根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
*
* @param record
* @return
*/
T persist(T record);

/**
* 批量操作.根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
*
* @param recordList
* @return
*/
List<T> persist(List<T> recordList);

/**
* 根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
*
* @param record
* @return
*/
T persistSelective(T record);

/**
* 批量操作.根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
*
* @param recordList
* @return
*/
List<T> persistSelective(List<T> recordList);

//
// select
// ----------------------------------------------------------------------------------------------------
/**
* 根据主键查询
*
* @param id 不能为空
* @return
*/
T get(Long id);

/**
* 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常
*
* @param record
* @return
*/
T get(T record);

/**
* 根据字段和值查询 返回一个
* @param key 不能为空
* @param value 不能为空
* @return
*/
T get(String key, Object value);

/**
* 根据主键字符串进行查询
*
* @param ids 如 "1,2,3,4"
* @return
*/
List<T> select(String ids);

/**
* 根据实体中的属性值进行查询
*
* @param record
* @return
*/
List<T> select(T record);

/**
* 根据属性和值查询
*
* @param key
* @param value
* @return
*/
List<T> select(String key, Object value);

/**
* 根据实体中的属性值进行分页查询
*
* @param record
* @param pageNum
* @param pageSize
* @return
*/
List<T> select(T record, int pageNum, int pageSize);

/**
* 查询全部结果
*
* @return
*/
List<T> selectAll();

/**
* 根据实体中的属性查询总数
*
* @param record
* @return
*/
int count(T record);

}

然后是实现类BaseService,以后的开发中,Service接口实现Service<T>,Service实现类继承BaseService<T>。

package com.lyyzoo.core.base;

import com.github.pagehelper.PageHelper;
import com.lyyzoo.core.Constants;
import com.lyyzoo.core.util.Arrays;
import com.lyyzoo.core.util.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import javax.persistence.Id;
import java.lang.reflect.Field;
import java.util.List;

/**
* 基础Service实现类
*
* @name BaseServiceImpl
* @version 1.0
* @author bojiangzhou 2018-01-04
*/
public abstract class BaseService<T> implements Service<T> {

//@Qualifier("sunnyMapper")
@Autowired
private Mapper<T> mapper;

private Class<T> entityClass;

@SuppressWarnings("unchecked")
@PostConstruct
public void init() {
this.entityClass = Reflections.getClassGenericType(getClass());
}

//
// insert
// ----------------------------------------------------------------------------------------------------
@Transactional(rollbackFor = Exception.class)
public T insert(T record) {
mapper.insert(record);
return record;
}

@Transactional(rollbackFor = Exception.class)
public List<T> insert(List<T> recordList) {
mapper.insertList(recordList);
return recordList;
}

@Transactional(rollbackFor = Exception.class)
public T insertSelective(T record) {
mapper.insertSelective(record);
return record;
}

@Transactional(rollbackFor = Exception.class)
public List<T> insertSelective(List<T> recordList) {
// 由于Mapper暂未提供Selective的批量插入,此处循环查询. 当然也可参考InsertListMapper自己实现.
for(T record : recordList){
mapper.insertSelective(record);
}
return recordList;
}

//
// update
// ----------------------------------------------------------------------------------------------------
@Transactional(rollbackFor = Exception.class)
public T update(T record) {
mapper.updateByPrimaryKey(record);
return record;
}

@Transactional(rollbackFor = Exception.class)
public List<T> update(List<T> recordList) {
// Mapper暂未提供批量更新,此处循实现
for(T record : recordList){
mapper.updateByPrimaryKey(record);
}
return recordList;
}

@Transactional(rollbackFor = Exception.class)
public T updateSelective(T record) {
mapper.updateByPrimaryKeySelective(record);
return record;
}

@Transactional(rollbackFor = Exception.class)
public List<T> updateSelective(List<T> recordList) {
// Mapper暂未提供批量更新,此处循实现
for(T record : recordList){
mapper.updateByPrimaryKeySelective(record);
}
return recordList;
}

//
// delete
// ----------------------------------------------------------------------------------------------------
@Transactional(rollbackFor = Exception.class)
public int delete(Long id) {
return mapper.deleteByIds(id.toString());
}

@Transactional(rollbackFor = Exception.class)
public int delete(String ids) {
return mapper.deleteByIds(ids);
}

@Transactional(rollbackFor = Exception.class)
public int delete(Long[] ids) {
return mapper.deleteByIds(Arrays.join(ids));
}

@Transactional(rollbackFor = Exception.class)
public int delete(T record) {
return mapper.delete(record);
}

@Transactional(rollbackFor = Exception.class)
public int delete(List<T> recordList) {
int count = 0;
for(T record : recordList){
mapper.delete(record);
count++;
}
return count;
}

//
// all operate. insert or update or delete
// ----------------------------------------------------------------------------------------------------
@Transactional(rollbackFor = Exception.class)
public T persist(T record) {
BaseDTO dto = (BaseDTO) record;
switch (dto.get_operate()) {
case Constants.Operation.ADD:
insert(record);
break;
case Constants.Operation.UPDATE:
update(record);
break;
case Constants.Operation.DELETE:
delete(record);
break;
default:
break;
}
dto.set_operate(null);
return record;
}

@Transactional(rollbackFor = Exception.class)
public List<T> persist(List<T> recordList) {
for(T record : recordList){
BaseDTO dto = (BaseDTO) record;
switch (dto.get_operate()) {
case Constants.Operation.ADD:
insert(record);
break;
case Constants.Operation.UPDATE:
update(record);
break;
case Constants.Operation.DELETE:
delete(record);
break;
default:
break;
}
dto.set_operate(null);
}
return recordList;
}

@Transactional(rollbackFor = Exception.class)
public T persistSelective(T record) {
BaseDTO dto = (BaseDTO) record;
switch (dto.get_operate()) {
case Constants.Operation.ADD:
insertSelective(record);
break;
case Constants.Operation.UPDATE:
updateSelective(record);
break;
case Constants.Operation.DELETE:
delete(record);
break;
default:
break;
}
dto.set_operate(null);
return record;
}

@Transactional(rollbackFor = Exception.class)
public List<T> persistSelective(List<T> recordList) {
for(T record : recordList){
BaseDTO dto = (BaseDTO) record;
switch (dto.get_operate()) {
case Constants.Operation.ADD:
insertSelective(record);
break;
case Constants.Operation.UPDATE:
updateSelective(record);
break;
case Constants.Operation.DELETE:
delete(record);
break;
default:
break;
}
dto.set_operate(null);
}
return recordList;
}

//
// select
// ----------------------------------------------------------------------------------------------------
public T get(Long id) {
T entity = null;
try {
entity = entityClass.newInstance();
Field idField = Reflections.getFieldByAnnotation(entityClass, Id.class);
idField.set(entity, id);
} catch (Exception e) {
e.printStackTrace();
}

return mapper.selectByPrimaryKey(entity);
}

public T get(T record) {
return mapper.selectOne(record);
}

public T get(String key, Object value) {
T entity = null;
try {
entity = entityClass.newInstance();
Field field = Reflections.getField(entityClass, key);
field.set(entity, value);
} catch (Exception e) {
e.printStackTrace();
}

return mapper.selectOne(entity);
}

public List<T> select(String ids) {
return mapper.selectByIds(ids);
}

public List<T> select(T record) {

return mapper.select(record);
}

public List<T> select(String key, Object value) {
T entity = null;
try {
entity = entityClass.newInstance();
Field field = Reflections.getField(entityClass, key);
field.set(entity, value);
} catch (Exception e) {
e.printStackTrace();
}
return mapper.select(entity);
}

public List<T> select(T record, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
return mapper.select(record);
}

public List<T> selectAll() {
return mapper.selectAll();
}

public int count(T record) {
return mapper.selectCount(record);
}

}

BaseService的实现用到了反射工具类Reflections:

package com.lyyzoo.core.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
* 反射工具类.
*
* @version 1.0
* @author bojiangzhou 2018-01-06
*/

public abstract class Reflections {

private static Logger logger = LoggerFactory.getLogger(Reflections.class);

/**
* 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处. 如无法找到, 返回Object.class.
*
* @param clazz class类
*
* @return the 返回第一个声明的泛型类型. 如果没有,则返回Object.class
*/
@SuppressWarnings("unchecked")
public static Class getClassGenericType(final Class clazz) {
return getClassGenericType(clazz, 0);
}

/**
* 通过反射, 获得Class定义中声明的父类的泛型参数的类型. 如无法找到, 返回Object.class.
*
* @param clazz class类
*
* @param index 获取第几个泛型参数的类型,默认从0开始,即第一个
*
* @return 返回第index个泛型参数类型.
*/
public static Class getClassGenericType(final Class clazz, final int index) {
Type genType = clazz.getGenericSuperclass();

if (!(genType instanceof ParameterizedType)) {
return Object.class;
}

Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

if (index >= params.length || index < 0) {
logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length);
return Object.class;
}
if (!(params[index] instanceof Class)) {
logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
return Object.class;
}

return (Class) params[index];
}

/**
* 根据注解类型获取实体的Field
*
* @param entityClass 实体类型
*
* @param annotationClass 注解类型
*
* @return 返回第一个有该注解类型的Field,如果没有则返回null.
*/
@SuppressWarnings("unchecked")
public static Field getFieldByAnnotation(Class entityClass, Class annotationClass) {
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
if (field.getAnnotation(annotationClass) != null) {
makeAccessible(field);
return field;
}
}
return null;
}

/**
* 获取实体的字段
*
* @param entityClass 实体类型
*
* @param fieldName 字段名称
*
* @return 该字段名称对应的字段,如果没有则返回null.
*/
public static Field getField(Class entityClass, String fieldName){
try {
Field field = entityClass.getDeclaredField(fieldName);
makeAccessible(field);
return field;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}

/**
* 改变private/protected的成员变量为public.
*/
public static void makeAccessible(Field field) {
if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
field.setAccessible(true);
}
}

}

7、数据持久化测试

① 实体映射

实体类按照如下规则和数据库表进行转换,注解全部是JPA中的注解:

  • 表名默认使用类名,驼峰转下划线(只对大写字母进行处理),如UserInfo默认对应的表名为user_info

  • 表名可以使@Table(name = "tableName")进行指定,对不符合第一条默认规则的可以通过这种方式指定表名。

  • 字段默认和@Column一样,都会作为表字段,表字段默认为Java对象的Field名字驼峰转下划线形式。

  • 可以使用@Column(name = "fieldName")指定不符合第3条规则的字段名。

  • 使用@Transient注解可以忽略字段,添加该注解的字段不会作为表字段使用,注意,如果没有与表关联,一定要用@Transient标注。

  • 建议一定是有一个@Id注解作为主键的字段,可以有多个@Id注解的字段作为联合主键。

  • 默认情况下,实体类中如果不存在包含@Id注解的字段,所有的字段都会作为主键字段进行使用(这种效率极低)。

  • 由于基本类型,如int作为实体类字段时会有默认值0,而且无法消除,所以实体类中建议不要使用基本类型。

User实体主要加了@Table注解,映射表名;然后在userId上标注主键注解;其它字段如果没加@Transient注解的默认都会作为表字段。

package com.lyyzoo.core.system.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lyyzoo.core.base.BaseDTO;
import com.lyyzoo.core.util.Dates;

import javax.persistence.*;
import java.util.Date;
import java.util.List;

/**
* 系统用户
*
* @name User
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Table(name = "SYS_USER")
public class User extends BaseDTO {
private static final long serialVersionUID = -7395431342743009038L;

/**
* 用户ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@OrderBy("DESC")
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 生日
*/
@JsonFormat(pattern = Dates.Pattern.DATE)
private Date birthday;
/**
* 性别:1-男/0-女
*/
private Integer sex;
/**
* 是否启用:1/0
*/
private Integer enabled;

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

public Integer getSex() {
return sex;
}

public void setSex(Integer sex) {
this.sex = sex;
}

public Integer getEnabled() {
return enabled;
}

public void setEnabled(Integer enabled) {
this.enabled = enabled;
}

}

② 创建表结构

CREATE TABLE `sys_user` (
`USER_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '表ID,主键,供其他表做外键',
`USERNAME` varchar(30) NOT NULL COMMENT '用户名',
`PASSWORD` varchar(100) NOT NULL COMMENT '密码',
`NICKNAME` varchar(30) NOT NULL COMMENT '用户名称',
`BIRTHDAY` date DEFAULT NULL COMMENT '生日',
`SEX` int(1) DEFAULT NULL COMMENT '性别:1-男;0-女',
`ENABLED` int(1) NOT NULL DEFAULT '1' COMMENT '启用标识:1/0',
`VERSION_NUMBER` int(11) NOT NULL DEFAULT '1' COMMENT '行版本号,用来处理锁',
`CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`CREATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '创建人',
`UPDATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '更新人',
`UPDATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`ATTRIBUTE1` varchar(150) DEFAULT NULL,
`ATTRIBUTE2` varchar(150) DEFAULT NULL,
`ATTRIBUTE3` varchar(150) DEFAULT NULL,
`ATTRIBUTE4` varchar(150) DEFAULT NULL,
`ATTRIBUTE5` varchar(150) DEFAULT NULL,
`ATTRIBUTE6` varchar(150) DEFAULT NULL,
`ATTRIBUTE7` varchar(150) DEFAULT NULL,
`ATTRIBUTE8` varchar(150) DEFAULT NULL,
`ATTRIBUTE9` varchar(150) DEFAULT NULL,
`ATTRIBUTE10` varchar(150) DEFAULT NULL,
PRIMARY KEY (`USER_ID`),
UNIQUE KEY `USERNAME` (`USERNAME`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系统用户';

③ 创建UserMapper

在system.mapper下创建UserMapper接口,继承Mapper<User>:

package com.lyyzoo.core.system.mapper;

import com.lyyzoo.core.base.Mapper;
import com.lyyzoo.core.system.dto.User;

/**
*
* @name UserMapper
* @version 1.0
* @author bojiangzhou 2018-01-06
*/
public interface UserMapper extends Mapper<User> {

}

④ 创建UserService

在system.service下创建UserService接口,只需继承Service<User>接口即可。

package com.lyyzoo.core.system.service;

import com.lyyzoo.core.base.Service;
import com.lyyzoo.core.system.dto.User;

/**
* 用户Service接口
*
* @version 1.0
* @author bojiangzhou 2018-01-06
*/
public interface UserService extends Service<User> {

}

在system.service.impl下创建UserServiceImpl实现类,继承BaseService<User>类,实现UserService接口。同时加上@Service注解。

package com.lyyzoo.core.system.service.impl;

import org.springframework.stereotype.Service;

import com.lyyzoo.core.base.BaseService;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.system.service.UserService;

/**
* 用户Service实现类
*
* @version 1.0
* @author bojiangzhou 2018-01-06
*/
@Service
public class UserServiceImpl extends BaseService<User> implements UserService {

}

⑤ 修改UserController,注入UserService,增加一些测试API

package com.lyyzoo.core.system.controller;

import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.BaseEnums;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.system.service.UserService;
import com.lyyzoo.core.util.Results;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

/**
* 用户Controller
*
* @version 1.0
* @author bojiangzhou 2017-12-31
*/
@RequestMapping
@RestController
public class UserController extends BaseController {

@Autowired
private UserService userService;

@PostMapping("/sys/user/queryAll")
public Result queryAll(){
List<User> list = userService.selectAll();
return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
}

@RequestMapping("/sys/user/queryOne/{userId}")
public Result queryOne(@PathVariable Long userId){
User user = userService.get(userId);
return Results.successWithData(user);
}

@PostMapping("/sys/user/save")
public Result save(@Valid @RequestBody User user){
user = userService.insertSelective(user);
return Results.successWithData(user);
}

@PostMapping("/sys/user/update")
public Result update(@Valid @RequestBody List<User> user){
user = userService.persistSelective(user);
return Results.successWithData(user);
}

@RequestMapping("/sys/user/delete")
public Result delete(User user){
userService.delete(user);
return Results.success();
}

@RequestMapping("/sys/user/delete/{userId}")
public Result delete(@PathVariable Long userId){
userService.delete(userId);
return Results.success();
}

}

⑥ 测试结果

查询所有:

批量保存/修改:

8、代码生成器

使用代码生成器来生成基础的代码结构,生成DTO、XML等等。

MyBatis官方提供了代码生成器MyBatis Generator,但一般需要定制化。MyBatis Generator

我这里从网上找了一个使用起来比较方便的界面工具,可生成DTO、Mapper、Mapper.xml,生成之后还需做一些小调整。另需要自己创建对应的Service、Controller。之后有时间再重新定制化一个符合本项目的代码生成器。

mybatis-generator界面工具

四、日志及全局异常处理

在前面的测试中,会发现控制台输出的日志不怎么友好,有很多日志也没有输出,不便于查找排查问题。对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。

先贴出一些参考资料:

  logback 配置详解

  日志组件slf4j介绍及配置详解

  Java常用日志框架介绍

1、日志框架简介

Java有很多常用的日志框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。有时候你可能会感觉有点混乱,下面简单介绍下。

  • Log4j:Apache Log4j是一个基于Java的日志记录工具,是Apache软件基金会的一个项目。

  • Log4j 2:Apache Log4j 2是apache开发的一款Log4j的升级产品。

  • Commons Logging:Apache基金会所属的项目,是一套Java日志接口。

  • Slf4j:类似于Commons Logging,是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)。

  • Logback:一套日志组件的实现(slf4j阵营)。

Commons Logging和Slf4j是日志门面,提供一个统一的高层接口,为各种loging API提供一个简单统一的接口。log4j和Logback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,做到解耦。

比较常用的组合使用方式是Slf4j与Logback组合使用,Commons Logging与Log4j组合使用。

基于下面的一些优点,选用Slf4j+Logback的日志框架:

  • 更快的执行速度,Logback重写了内部的实现,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了

  • 自动清除旧的日志归档文件,通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,你就可以控制日志归档文件的最大数量

  • Logback拥有远比log4j更丰富的过滤能力,可以不用降低日志级别而记录低级别中的日志。

  • Logback必须配合Slf4j使用。由于Logback和Slf4j是同一个作者,其兼容性不言而喻。

  • 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。

2、配置日志

可以看到,只要集成了spring-boot-starter-web,就引入了spring-boot-starter-logging,即slf4j和logback。

其它的几个包:jcl-over-slf4j,代码直接调用common-logging会被桥接到slf4j;jul-to-slf4j,代码直接调用java.util.logging会被桥接到slf4j;log4j-over-slf4j,代码直接调用log4j会被桥接到slf4j。

还需引入janino,如果不加入这个包会报错。

在resources下添加logback.xml配置文件,Logback默认会查找classpath下的logback.xml文件。

具体配置如下,有较详细的注释,很容易看懂。可以通过application.properties配置日志记录级别、日志输出文件目录等。

<?xml version="1.0" encoding="UTF-8"?>

<!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 -->
<!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志 -->
<!-- scan 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 -->
<!-- scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration debug="false" scan="false" scanPeriod="5 minutes">

<!-- 引入配置文件 -->
<property resource="application.properties"/>
<property resource="application-${app.env:-dev}.properties"/>

<property name="app.name" value="${app.name:-sunny}"/>
<property name="app.env" value="${app.env:-dev}"/>

<!-- 日志记录级别 -->
<property name="logback_level" value="${logback.level:-DEBUG}"/>
<!-- 是否输出日志到文件 -->
<property name="logback_rolling" value="${logback.rolling:-false}"/>
<!-- 设置日志输出目录 -->
<property name="logback_rolling_path" value="${logback.rolling.path:-/data/logs}"/>
<!-- 日志文件最大大小 -->
<property name="logback_max_file_size" value="${logback.max_file_size:-10MB}"/>
<!-- 格式化输出:%d:表示日期,%thread:表示线程名,%-5level:级别从左显示5个字符宽度,%logger:日志输出者的名字(通常是所在类的全名),%L:输出代码中的行号,%msg:日志消息,%n:换行符 -->
<property name="logback_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger %L -| %msg%n"/>

<if condition='p("logback_rolling").equals("true")'>
<then>
<!-- 滚动记录文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logback_rolling_path}/${app.name}.log</file>
<!-- rollingPolicy:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名 -->
<!-- TimeBasedRollingPolicy:最常用的滚动策略,它根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<fileNamePattern>${logback_rolling_path}/${app.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>

<!-- 日志文件的保存期限为30天 -->
<maxHistory>30</maxHistory>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- maxFileSize:这是活动文件的大小,默认值是10MB -->
<maxFileSize>${logback_max_file_size}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${logback_pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<root>
<appender-ref ref="FILE"/>
</root>
</then>
</if>

<!-- 将日志打印到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${logback_pattern}</pattern>
</encoder>
</appender>

<root level="${logback_level}">
<appender-ref ref="CONSOLE"/>
</root>

<contextName>${app.name}</contextName>

</configuration>

加入配置文件后,就可以看到控制台格式化后的日志输出,还可以看到具体代码行数等,比之前的友好多了。

同时,将日志滚动输出到日志文件,保留历史记录。可通过logback.rolling=false控制是否需要输出日志到文件。

3、使用Logger

配置好之后,就可以使用Logger来输出日志了,使用起来也是非常方便。

* 可以看到引入的包是slf4j.Logger,代码里并没有引用任何一个跟 Logback 相关的类,这便是使用 Slf4j的好处,在需要将日志框架切换为其它日志框架时,无需改动已有的代码。

* LoggerFactory 的 getLogger() 方法接收一个参数,以这个参数决定 logger 的名字,比如第二图中的日志输出。在为 logger 命名时,用类的全限定类名作为 logger name 是最好的策略,这样能够追踪到每一条日志消息的来源

* 可以看到,可以通过提供占位符,以参数化的方式打印日志,避免字符串拼接的不必要损耗,也无需通过logger.isDebugEnabled()这种方式判断是否需要打印。

4、全局异常处理

现在有一个问题,当日志级别设置到INFO级别后,只会输出INFO以上的日志,如INFO、WARN、ERROR,这没毛病,问题是,程序中抛出的异常堆栈(运行时异常)都没有打印了,不利于排查问题。

而且,在某些情况下,我们在Service中想直接把异常往Controller抛出不做处理,但我们不能直接把异常信息输出到客户端,这是非常不友好的。

所以,在config下建一个GlobalExceptionConfig作为全局统一异常处理。主要处理了自定义的ServiceException、AuthorityException、BaseException,以及系统的NoHandlerFoundException和Exception异常。

package com.lyyzoo.core.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.constants.BaseEnums;
import com.lyyzoo.core.exception.AuthorityException;
import com.lyyzoo.core.exception.BaseException;
import com.lyyzoo.core.exception.ServiceException;
import com.lyyzoo.core.util.Results;

/**
* 全局异常处理
*
* @author bojiangzhou 2018-02-06
* @version 1.0
*/
@RestControllerAdvice
public class GlobalExceptionConfig {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionConfig.class);

/**
* 处理 ServiceException 异常
*/
@ExceptionHandler(ServiceException.class)
public Result handleServiceException(ServiceException e){
Result result = Results.failure(e.getCode(), e.getMessage());
result.setStatus(HttpStatus.BAD_REQUEST.value());
logger.info("ServiceException[code: {}, message: {}]", e.getCode(), e.getMessage());
return result;
}

/**
* 处理 AuthorityException 异常
*/
@ExceptionHandler(AuthorityException.class)
public Result handleAuthorityException(AuthorityException e){
Result result = Results.failure(BaseEnums.FORBIDDEN.code(), BaseEnums.FORBIDDEN.desc());
result.setStatus(HttpStatus.FORBIDDEN.value());
logger.info("AuthorityException[code: {}, message: {}]", e.getCode(), e.getMessage());
return result;
}

/**
* 处理 NoHandlerFoundException 异常. <br/>
* 需配置 [spring.mvc.throw-exception-if-no-handler-found=true]
* 需配置 [spring.resources.add-mappings=false]
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result handleNotFoundException(NoHandlerFoundException e){
Result result = Results.failure(BaseEnums.NOT_FOUND.code(), BaseEnums.NOT_FOUND.desc());
result.setStatus(HttpStatus.NOT_FOUND.value());
logger.info(e.getMessage());
return result;
}

/**
* 处理 BaseException 异常
*/
@ExceptionHandler(BaseException.class)
public Result handleBaseException(BaseException e){
Result result = Results.failure(e.getCode(), e.getMessage());
result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
logger.error("BaseException[code: {}, message: {}]", e.getCode(), e.getMessage(), e);
return result;
}

/**
* 处理 Exception 异常
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
Result result = Results.failure(BaseEnums.ERROR.code(), BaseEnums.ERROR.desc());
result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
logger.error(e.getMessage(), e);
return result;
}

}

看上面的代码,@ControllAdvice(@RestControllerAdvice可以返回ResponseBody),可看做Controller增强器,可以在@ControllerAdvice作用类下添加@ExceptionHandler,@InitBinder,@ModelAttribute注解的方法来增强Controller,都会作用在被 @RequestMapping 注解的方法上。

使用@ExceptionHandler 拦截异常,我们可以通过该注解实现自定义异常处理。在每个处理方法中,封装Result,返回对应的消息及状态码等。

通过Logger打印对应级别的日志,也可以看到控制台及日志文件中有异常堆栈的输出了。注意除了BaseException、Exception,其它的都只是打印了简单信息,且为INFO级别。Exception是ERROR级别,且打印了堆栈信息。

NoHandlerFoundException 是404异常,这里注意要先关闭DispatcherServlet的NotFound默认异常处理。

测试如下:这种返回结果就比较友好了。

    

五、数据库乐观锁

1、乐观锁

在并发修改同一条记录时,为避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存层加锁,要么在数据库层使用乐观锁,使用version作为更新依据【强制】。 —— 《阿里巴巴Java开发手册》

乐观锁,基于数据版本(version)记录机制实现,为数据库表增加一个"version"字段。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。提交数据时,提交的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

因此,这节就来处理BaseDTO中的"version"字段,通过增加一个mybatis插件来实现更新时版本号自动+1。

2、MyBatis插件介绍

MyBatis 允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下, MyBatis 允许使用插件来拦截的接口和方法包括以下几个:

  • Executor (update 、query 、flushStatements 、commit 、rollback 、getTransaction 、close 、isClosed)

  • ParameterHandler (getParameterObject 、setParameters)

  • ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)

  • StatementHandler (prepare 、parameterize 、batch update 、query)

MyBatis 插件实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理 。

  • setProperties:传递插件的参数,可以通过参数来改变插件的行为。

  • plugin:参数 target 就是要拦截的对象,作用就是给被拦截对象生成一个代理对象,并返回。

  • intercept:会覆盖所拦截对象的原方法,Invocation参数可以反射调度原来对象的方法,可以获取到很多有用的东西。

除了需要实现拦截器接口外,还需要给实现类配置拦截器签名。 使用 @Intercepts 和 @Signature 这两个注解来配置拦截器要拦截的接口的方法,接口方法对应的签名基本都是固定的。

@Intercepts 注解的属性是一个 @Signature  数组,可以在同 一个拦截器中同时拦截不同的接口和方法。

@Signature 注解包含以下三个属性。

  • type:设置拦截的接口,可选值是前面提到的4个接口 。

  • method:设置拦截接口中的方法名, 可选值是前面4个接口对应的方法,需要和接口匹配 。

  • args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法 。

3、数据版本插件

要实现版本号自动更新,我们需要在SQL被执行前修改SQL,因此我们需要拦截的就是 StatementHandler  接口的 prepare 方法,该方法会在数据库执行前被调用,优先于当前接口的其它方法而被执行。

在 core.plugin 包下新建一个VersionPlugin插件,实现Interceptor拦截器接口。

该接口方法签名如下:

在 interceptor 方法中对 UPDATE 类型的操作,修改原SQL,加入version,修改后的SQL类似下图,更新时就会自动将version+1。同时带上version条件,如果该版本号小于数据库记录版本号,则不会更新。

VersionInterceptor插件:

package com.lyyzoo.core.plugins;

import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.List;
import java.util.Properties;

/**
* 乐观锁:数据版本插件
*
* @version 1.0
* @author bojiangzhou 2018-02-10
*/
@Intercepts(
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
)
public class VersionInterceptor implements Interceptor {

private static final String VERSION_COLUMN_NAME = "version";

private static final Logger logger = LoggerFactory.getLogger(VersionInterceptor.class);

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 StatementHandler,实际是 RoutingStatementHandler
StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget());
// 包装原始对象,便于获取和设置属性
MetaObject metaObject = SystemMetaObject.forObject(handler);
// MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各种配置信息
MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// SQL类型
SqlCommandType sqlType = ms.getSqlCommandType();
if(sqlType != SqlCommandType.UPDATE) {
return invocation.proceed();
}
// 获取版本号
Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);
if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){
return invocation.proceed();
}
// 获取绑定的SQL
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
// 原始SQL
String originalSql = boundSql.getSql();
// 加入version的SQL
originalSql = addVersionToSql(originalSql, originalVersion);
// 修改 BoundSql
metaObject.setValue("delegate.boundSql.sql", originalSql);

// proceed() 可以执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target, args)方法
return invocation.proceed();
}

/**
* Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象.
*
* @param target 被拦截的对象
* @return 代理对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

/**
* 设置参数
*/
@Override
public void setProperties(Properties properties) {

}

/**
* 获取代理的原始对象
*
* @param target
* @return
*/
private static Object processTarget(Object target) {
if(Proxy.isProxyClass(target.getClass())) {
MetaObject mo = SystemMetaObject.forObject(target);
return processTarget(mo.getValue("h.target"));
}
return target;
}

/**
* 为原SQL添加version
*
* @param originalSql 原SQL
* @param originalVersion 原版本号
* @return 加入version的SQL
*/
private String addVersionToSql(String originalSql, Object originalVersion){
try{
Statement stmt = CCJSqlParserUtil.parse(originalSql);
if(!(stmt instanceof Update)){
return originalSql;
}
Update update = (Update)stmt;
if(!contains(update)){
buildVersionExpression(update);
}
Expression where = update.getWhere();
if(where != null){
AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion));
update.setWhere(and);
}else{
update.setWhere(buildVersionEquals(originalVersion));
}
return stmt.toString();
}catch(Exception e){
logger.error(e.getMessage(), e);
return originalSql;
}
}

private boolean contains(Update update){
List<Column> columns = update.getColumns();
for(Column column : columns){
if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){
return true;
}
}
return false;
}

private void buildVersionExpression(Update update){
// 列 version
Column versionColumn = new Column();
versionColumn.setColumnName(VERSION_COLUMN_NAME);
update.getColumns().add(versionColumn);

// 值 version+1
Addition add = new Addition();
add.setLeftExpression(versionColumn);
add.setRightExpression(new LongValue(1));
update.getExpressions().add(add);
}

private Expression buildVersionEquals(Object originalVersion){
Column column = new Column();
column.setColumnName(VERSION_COLUMN_NAME);

// 条件 version = originalVersion
EqualsTo equal = new EqualsTo();
equal.setLeftExpression(column);
equal.setRightExpression(new LongValue(originalVersion.toString()));
return equal;
}

}

之后还需配置该插件,只需要在MyBatisConfig中加入该配置即可。

最后,如果版本不匹配,更新失败,需要往外抛出异常提醒,所以修改BaseService的update方法,增加检查更新是否失败。

最后,能不用插件尽量不要用插件,因为它将修改MyBatis的底层设计。插件生成的是层层代理对象的责任链模式,通过反射方法运行,会有一定的性能消耗。

我们也可以修改 tk.mapper 生成SQL的方法,加入version,这里通过插件方式实现乐观锁主要是不为了去修改 mapper 的底层源码,比较方便。

六、Druid数据库连接池

创建数据库连接是一个很耗时的操作,也很容易对数据库造成安全隐患。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响程序的性能指标。

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。数据库连接池能明显提高对数据库操作的性能。

参考:

  Druid常见问题集锦

  常用数据库连接池 (DBCP、c3p0、Druid) 配置说明

1、Druid

Druid首先是一个数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQLParser。Druid支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、Postgresql、SQLServer、H2等等。 Druid针对Oracle和MySql做了特别优化,比如Oracle的PSCache内存占用优化,MySql的ping检测优化。Druid在监控、可扩展性、稳定性和性能方面都有明显的优势。Druid提供了Filter-Chain模式的扩展API,可以自己编写Filter拦截JDBC中的任何方法,可以在上面做任何事情,比如说性能监控、SQL审计、用户名密码加密、日志等等。

2、配置

Druid配置到core模块下,只需在application.properties中添加如下配置即可,大部分配置是默认配置,可更改。有详细的注释,比较容易理解。

####################################
# Druid
####################################
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 初始化连接大小[0]
spring.datasource.druid.initial-size=1
# 最小空闲连接数[0]
spring.datasource.druid.min-idle=1
# 最大连接数[8]
spring.datasource.druid.max-active=20

# 配置获取连接等待超时的时间(毫秒)[-1]
spring.datasource.druid.max-wait=60000
# 查询超时时间(秒)
spring.datasource.druid.query-timeout=90

# 用来检测连接是否有效的sql,要求是一个查询语句
spring.datasource.druid.validation-query=SELECT 'x'
# 申请连接时检测连接可用性[false]
spring.datasource.druid.test-on-borrow=false
# 归还连接检测[false]
spring.datasource.druid.test-on-return=false
# 超时是否检测连接可用性[true]
spring.datasource.druid.test-while-idle=true

# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接 (毫秒)
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间(毫秒,默认30分钟)
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall
spring.datasource.druid.filters=stat,wall,slf4j
# 合并多个DruidDataSource的监控数据
spring.datasource.druid.use-global-data-source-stat=true

# 是否缓存PreparedStatement. PSCache对支持游标的数据库性能提升巨大,比如说oracle.在mysql下建议关闭.
spring.datasource.druid.pool-prepared-statements=false
# 每个连接上PSCache的大小
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20

# StatViewServlet [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE]
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
# 监控页面的用户名和密码
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.reset-enable=false

# StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter]
spring.datasource.druid.filter.stat.db-type=mysql
#慢SQL记录
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# SQL合并
spring.datasource.druid.filter.stat.merge-sql=false

# WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter]
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false

之后启动项目在地址栏输入/druid/index.html并登录就可以看到Druid监控页面:

七、Redis缓存

对于如今的一个中小型系统来说,至少也需要一个缓存来缓存热点数据,加快数据的访问数据,这里选用Redis做缓存数据库。在以后可以使用Redis做分布式缓存、做Session共享等。

1、SpringBoot的缓存支持

Spring定义了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口,Cache接口包含缓存的各种操作。

针对不同的缓存技术,需要实现不同的CacheManager,Redis缓存则提供了RedisCacheManager的实现。

我将redis缓存功能放到sunny-starter-cache模块下,cache模块下可以有多种缓存技术,同时,对于其它项目来说,缓存是可插拔的,想用缓存直接引入cache模块即可。

首先引入Redis的依赖:

SpringBoot已经默认为我们自动配置了多个CacheManager的实现,在autoconfigure.cache包下。在Spring Boot 环境下,使用缓存技术只需在项目中导入相关的依赖包即可。

在 RedisCacheConfiguration 里配置了默认的 CacheManager;SpringBoot提供了默认的redis配置,RedisAutoConfiguration 是Redis的自动化配置,比如创建连接池、初始化RedisTemplate等。

2、Redis 配置及声明式缓存支持

Redis 默认配置了 RedisTemplate 和 StringRedisTemplate ,其使用的序列化规则是 JdkSerializationRedisSerializer,缓存到redis后,数据都变成了下面这种样式,非常不易于阅读。

因此,重新配置RedisTemplate,使用 Jackson2JsonRedisSerializer 来序列化 Key 和 Value。同时,增加HashOperations、ValueOperations等Redis数据结构相关的操作,这样比较方便使用。

package com.lyyzoo.cache.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* Redis配置.
*
* 使用@EnableCaching开启声明式缓存支持. 之后就可以使用 @Cacheable/@CachePut/@CacheEvict 注解缓存数据.
*
* @author bojiangzhou 2018-02-11
* @version 1.0
*/
@Configuration
@EnableCaching
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;

/**
* 覆盖默认配置 RedisTemplate,使用 String 类型作为key,设置key/value的序列化规则
*/
@Bean
@SuppressWarnings("unchecked")
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);

// 使用 Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

// 设置value的序列化规则和key的序列化规则
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();

return redisTemplate;
}

@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}

@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}

@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}

@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}

@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}

@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
cacheManager.setUsePrefix(true);
return cacheManager;
}

}

同时,使用@EnableCaching开启声明式缓存支持,这样就可以使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,通过在代码中添加下面的一些注解,达到缓存的效果。

  • @Cacheable:在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。

  • @CachePut:将方法的返回值放到缓存中。

  • @CacheEvict:删除缓存中的数据。

Redis服务器相关的一些配置可在application.properties中进行配置:

3、Redis工具类

添加一个Redis的统一操作工具,主要是对redis的常用数据类型操作类做了一个归集。

ValueOperations用于操作String类型,HashOperations用于操作hash数据,ListOperations操作List集合,SetOperations操作Set集合,ZSetOperations操作有序集合。

关于redis的key命令和数据类型可参考我的学习笔记:

Redis 学习(一) —— 安装、通用key操作命令

Redis 学习(二) —— 数据类型及操作

package com.lyyzoo.cache.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Redis 操作工具
*
* @version 1.0
* @author bojiangzhou 2018-02-12
*/
@Component
public class RedisOperator {

@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ValueOperations<String, String> valueOperator;
@Autowired
private HashOperations<String, String, Object> hashOperator;
@Autowired
private ListOperations<String, Object> listOperator;
@Autowired
private SetOperations<String, Object> setOperator;
@Autowired
private ZSetOperations<String, Object> zSetOperator;

/**
* 默认过期时长,单位:秒
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 24;

/** 不设置过期时长 */
public final static long NOT_EXPIRE = -1;

/**
* Redis的根操作路径
*/
@Value("${redis.root:sunny}")
private String category;

public RedisOperator setCategory(String category) {
this.category = category;
return this;
}

/**
* 获取Key的全路径
*
* @param key key
* @return full key
*/
public String getFullKey(String key) {
return this.category + ":" + key;
}

//
// key
// ------------------------------------------------------------------------------
/**
* 判断key是否存在
*
* <p>
* <i>exists key</i>
*
* @param key key
*/
public boolean existsKey(String key) {
return redisTemplate.hasKey(getFullKey(key));
}

/**
* 判断key存储的值类型
*
* <p>
* <i>type key</i>
*
* @param key key
* @return DataType[string、list、set、zset、hash]
*/
public DataType typeKey(String key){
return redisTemplate.type(getFullKey(key));
}

/**
* 重命名key. 如果newKey已经存在,则newKey的原值被覆盖
*
* <p>
* <i>rename oldKey newKey</i>
*
* @param oldKey oldKeys
* @param newKey newKey
*/
public void renameKey(String oldKey, String newKey){
redisTemplate.rename(getFullKey(oldKey), getFullKey(newKey));
}

/**
* newKey不存在时才重命名.
*
* <p>
* <i>renamenx oldKey newKey</i>
*
* @param oldKey oldKey
* @param newKey newKey
* @return 修改成功返回true
*/
public boolean renameKeyNx(String oldKey, String newKey){
return redisTemplate.renameIfAbsent(getFullKey(oldKey), getFullKey(newKey));
}

/**
* 删除key
*
* <p>
* <i>del key</i>
*
* @param key key
*/
public void deleteKey(String key){
redisTemplate.delete(key);
}

/**
* 删除key
*
* <p>
* <i>del key1 key2 ...</i>
*
* @param keys 可传入多个key
*/
public void deleteKey(String ... keys){
Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet());
redisTemplate.delete(ks);
}

/**
* 删除key
*
* <p>
* <i>del key1 key2 ...</i>
*
* @param keys key集合
*/
public void deleteKey(Collection<String> keys){
Set<String> ks = keys.stream().map(k -> getFullKey(k)).collect(Collectors.toSet());
redisTemplate.delete(ks);
}

/**
* 设置key的生命周期,单位秒
*
* <p>
* <i>expire key seconds</i><br>
* <i>pexpire key milliseconds</i>
*
* @param key key
* @param time 时间数
* @param timeUnit TimeUnit 时间单位
*/
public void expireKey(String key, long time, TimeUnit timeUnit){
redisTemplate.expire(key, time, timeUnit);
}

/**
* 设置key在指定的日期过期
*
* <p>
* <i>expireat key timestamp</i>
*
* @param key key
* @param date 指定日期
*/
public void expireKeyAt(String key, Date date){
redisTemplate.expireAt(key, date);
}

/**
* 查询key的生命周期
*
* <p>
* <i>ttl key</i>
*
* @param key key
* @param timeUnit TimeUnit 时间单位
* @return 指定时间单位的时间数
*/
public long getKeyExpire(String key, TimeUnit timeUnit){
return redisTemplate.getExpire(key, timeUnit);
}

/**
* 将key设置为永久有效
*
* <p>
* <i>persist key</i>
*
* @param key key
*/
public void persistKey(String key){
redisTemplate.persist(key);
}

/**
*
* @return RedisTemplate
*/
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}

/**
*
* @return ValueOperations
*/
public ValueOperations<String, String> getValueOperator() {
return valueOperator;
}

/**
*
* @return HashOperations
*/
public HashOperations<String, String, Object> getHashOperator() {
return hashOperator;
}

/**
*
* @return ListOperations
*/
public ListOperations<String, Object> getListOperator() {
return listOperator;
}

/**
*
* @return SetOperations
*/
public SetOperations<String, Object> getSetOperator() {
return setOperator;
}

/**
*
* @return ZSetOperations
*/
public ZSetOperations<String, Object> getZSetOperator() {
return zSetOperator;
}

}

八、Swagger支持API文档

1、Swagger

做前后端分离,前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。

Swagger是一个简单又强大的能为你的Restful风格的Api生成文档的工具。在项目中集成这个工具,根据我们自己的配置信息能够自动为我们生成一个api文档展示页,可以在浏览器中直接访问查看项目中的接口信息,同时也可以测试每个api接口。

2、配置

我这里直接使用别人已经整合好的swagger-spring-boot-starter,快速方便。

参考:spring-boot-starter-swagger

新建一个sunny-starter-swagger模块,做到可插拔。

根据文档,一般只需要做些简单的配置即可:

但如果想要显示swagger-ui.html文档展示页,还必须注入swagger资源:

package com.lyyzoo.swagger.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.spring4all.swagger.EnableSwagger2Doc;

/**
* @version 1.0
* @author bojiangzhou 2018-02-19
*/
@Configuration
@EnableSwagger2Doc
@PropertySource(value = "classpath:application-swagger.properties")
public class SunnySwaggerConfig extends WebMvcConfigurerAdapter {
/**
* 注入swagger资源文件
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

}

3、使用

一般只需要在Controller加上swagger的注解即可显示对应的文档信息,如@Api、@ApiOperation、@ApiParam等。

常用注解参考:swagger-api-annotations

package com.lyyzoo.admin.system.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import com.lyyzoo.admin.system.dto.Menu;
import com.lyyzoo.admin.system.service.MenuService;
import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.util.Results;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

@Api(tags = "菜单管理")
@RequestMapping
@RestController
public class MenuController extends BaseController {

@Autowired
private MenuService service;

/**
* 查找单个用户
*
* @param menuId 菜单ID
* @return Result
*/
@ApiOperation("查找单个用户")
@ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path")
@GetMapping("/sys/menu/get/{menuId}")
public Result get(@PathVariable Long menuId){
Menu menu = service.selectById(menuId);
return Results.successWithData(menu);
}

/**
* 保存菜单
*
* @param menu 菜单
* @return Result
*/
@ApiOperation("保存菜单")
@PostMapping("/sys/menu/save")
public Result save(@ApiParam(name = "menu", value = "菜单")@RequestBody Menu menu){
menu = service.save(menu);
return Results.successWithData(menu);
}

/**
* 删除菜单
*
* @param menuId 菜单ID
* @return Result
*/
@ApiOperation("删除菜单")
@ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path")
@PostMapping("/sys/menu/delete/{menuId}")
public Result delete(@PathVariable Long menuId){
service.deleteById(menuId);
return Results.success();
}

}

之后访问swagger-ui.html页面就可以看到API文档信息了。

如果不需要swagger,在配置文件中配置swagger.enabled=false,或移除sunny-starter-swagger的依赖即可。

九、项目优化调整

到这里,项目最基础的一些功能就算完成了,但由于前期的一些设计不合理及未考虑周全等因素,对项目做一些调整。并参考《阿里巴巴Java开发手册》对代码做了一些优化。

1、项目结构

目前项目分为5个模块:

最外层的Sunny作为聚合模块负责管理所有子模块,方便统一构建。并且继承 spring-boot-starter-parent ,其它子模块则继承该模块,方便统一管理 Spring Boot 及本项目的版本。这里已经把Spring Boot的版本升到 1.5.10.RELEASE。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.lyyzoo</groupId>
<artifactId>sunny</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Sunny</name>
<description>Lyyzoo Base Application development platform</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>

<sunny.version>0.0.1-SNAPSHOT</sunny.version>
<springboot.version>1.5.10.RELEASE</springboot.version>
</properties>

<modules>
<module>sunny-starter</module>
<module>sunny-starter-core</module>
<module>sunny-starter-cache</module>
<module>sunny-starter-security</module>
<module>sunny-starter-admin</module>
<module>sunny-starter-swagger</module>
</modules>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

sunny-starter 则引入了其余几个模块,在开发项目时,只需要继承或引入sunny-starter即可,而无需一个个引入各个模块。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.lyyzoo</groupId>
<artifactId>sunny</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<groupId>com.lyyzoo.parent</groupId>
<artifactId>sunny-starter</artifactId>
<packaging>jar</packaging>

<name>sunny-starter</name>
<description>Sunny Parent</description>

<dependencies>
<!-- core -->
<dependency>
<groupId>com.lyyzoo.core</groupId>
<artifactId>sunny-starter-core</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- cache -->
<dependency>
<groupId>com.lyyzoo.cache</groupId>
<artifactId>sunny-starter-cache</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- security -->
<dependency>
<groupId>com.lyyzoo.security</groupId>
<artifactId>sunny-starter-security</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- admin -->
<dependency>
<groupId>com.lyyzoo.admin</groupId>
<artifactId>sunny-starter-admin</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>com.lyyzoo.swagger</groupId>
<artifactId>sunny-starter-swagger</artifactId>
<version>${sunny.version}</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

对于一个Spring Boot项目,应该只有一个入口,即 @SpringBootApplication 注解的类。经测试,其它的模块的配置文件application.properties的配置不会生效,应该是引用了入口模块的配置文件。

所以为了让各个模块的配置文件都能生效,只需使用 @PropertySource 引入该配置文件即可,每个模块都如此。在主模块定义的配置会覆盖其它模块的配置。

2、开发规范

我的自定义框架 || 基于Spring Boot || 第一步的更多相关文章

  1. 基于Spring Boot的注解驱动式公众号极速开发框架FastBootWeixin

    本框架基于Spring Boot实现,使用注解完成快速开发,可以快速的完成一个微信公众号,重新定义公众号开发. 在使用本框架前建议对微信公众号开发文档有所了解,不过在不了解公众号文档的情况下使用本框架 ...

  2. 快速搭建基于Spring Boot + Spring Security 环境

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 1.Spring Security 权限管理框架介绍 简介: Spring Security 提供了基于 ...

  3. 深入学习微框架:Spring Boot(转)

    转:http://www.infoq.com/cn/articles/microframeworks1-spring-boot/ 相关参考: https://spring.io/guides/gs/s ...

  4. step6----->往工程中添加spring boot项目------->修改pom.xml使得我的project是基于spring boot的,而非直接基于spring framework

    文章内容概述: spring项目组其实有多个projects,如spring IO platform用于管理external dependencies的版本,通过定义BOM(bill of mater ...

  5. 基于Spring Boot和Spring Cloud实现微服务架构学习

    转载自:http://blog.csdn.net/enweitech/article/details/52582918 看了几周Spring相关框架的书籍和官方demo,是时候开始总结下这中间的学习感 ...

  6. 基于Spring Boot和Spring Cloud实现微服务架构学习--转

    原文地址:http://blog.csdn.net/enweitech/article/details/52582918 看了几周spring相关框架的书籍和官方demo,是时候开始总结下这中间的学习 ...

  7. 基于Spring Boot的统一异常处理设计

    基于Spring Boot的统一异常处理设计 作者: Grey 原文地址:https://www.cnblogs.com/greyzeng/p/11733327.html Spring Boot中,支 ...

  8. 基于Spring Boot+Cloud构建微云架构

    前言 首先,最想说的是,当你要学习一套最新的技术时,官网的英文文档是学习的最佳渠道.因为网上流传的多数资料是官网翻译而来,很多描述的重点也都偏向于作者自身碰到的问题,这样就很容易让你理解和操作出现偏差 ...

  9. Spring Boot 第一弹,问候一下世界!!!

    持续原创输出,点击上方蓝字关注我吧 目录 前言 什么是Spring Boot? 如何搭建一个Spring Boot项目? 第一个程序 Hello World 依赖解读 什么是配置文件? 什么是启动类? ...

随机推荐

  1. 用node.js启动mock.js

    Node.js Node 是一个让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为与PHP.Python.Perl.Ruby 等服务端语言平起平坐的脚本语言.官网下载n ...

  2. ansible如果兼容Centos5

    基本安装 安装gcc,用于编译python源码 yum install gcc 更新python版本 centos自带python2.6版本,由于centos已对python深度依赖,所以更新pyth ...

  3. vmware虚拟机安装vmware tools

    为了在主机与虚拟机之间共享文件,需要安装vmware tools.在安装vmware tools的过程中遇到了几个坑,在此记录一下. 一.虚拟机关机情况下进行设置.虚拟机->设置->CD/ ...

  4. ionic2启动出现try again later

    新建IONIC2的项目时,启动只出现try again later 这个问题应该是安装依赖出现的,重装npm install 一次就可以了

  5. setting.xml

    <?xml version="1.0" encoding="UTF-8"?><settings xmlns="http://mave ...

  6. 爬虫(五)requests模块2

    引入 有些时候,我们在使用爬虫程序去爬取一些用户相关信息的数据(爬取张三“人人网”个人主页数据)时,如果使用之前requests模块常规操作时,往往达不到我们想要的目的,例如: #!/usr/bin/ ...

  7. Spring Boot Actuator [监控与管理]

    1. 如何添加 2. actuator 的原生端点(API) 2.1 应用类配置 2.1.1 http://localhost:8080/actuator/conditions 2.1.2 http: ...

  8. vue-router同路由$router.push不跳转一个简单解决方案

    vue-router同路由$router.push不跳转一个简单解决方案 vue-router跳转一般是这么写: toCurrentPage: function(thisId){ this.$rout ...

  9. Abp.vNext 权限备注

    Abp 内部是基于 asp.net core 基于 策略的  授权方式,每个权限为一个策略 权限分为: 1.定义权限(先定义权限组,后添加权限),每个模块都应该创建一个PermissionDefini ...

  10. PDF 补丁丁 0.6.0.3363 版发布(修复无法保存应用程序设置的问题)

    本测试版修复了上一测试版无法保存应用程序设置的问题,以及导出导入信息文件的若干小问题.