diff --git a/README.en.md b/README.en.md
index 4ed7873..d3a479a 100644
--- a/README.en.md
+++ b/README.en.md
@@ -5,7 +5,7 @@
-
+
@@ -20,17 +20,12 @@
## Introduction
-`spring boot demo` is a project for learning and practicing `spring boot`, including `66` demos, and `54` of them have been done.
+`spring boot demo` is a project for learning and practicing `spring boot`, including `66` demos, and `55` of them have been done.
-This project has integrated actuator (`monitoring`), admin (`visual monitoring`), logback (`log`), aopLog (`recording web request logs through AOP`), global exception handling (`json level and page level` ), freemarker (`template engine`), thymeleaf (`template engine`), Beetl (`template engine`), Enjoy (`template engine`), JdbcTemplate (`general JDBC operate database`), JPA (`powerful ORM framework `), mybatis (`powerful ORM framework`), Generic Mapper (`mybatis quick operation `), PageHelper (`powerful mybatis pagination plugin`), mybatis-plus (`mybatis quick operation`), BeetlSQL (`powerful ORM framework `), upload (`local file upload and qiniu cloud file upload`), redis (`cache`), ehcache (`cache`), email (`send various types of mail`), task (`basic scheduled tasks`), quartz (`dynamic management scheduled tasks`), xxl-job (`distributed scheduled tasks`), swagger (`API interface management and tests`), security (`RBAC-based Dynamic Rights Authentication`), SpringSession (`session sharing`), Zookeeper (`implement distributed locks by AOP`), RabbitMQ (`message queue`), Kafka (`message queue`), websocket (` server pushes the monitoring server status to front end `), socket.io (`chat room`), ureport2 (`Chinese-style report`), packaged into a `war` file, integrate ElasticSearch (`basic operations and advanced queries`), Async ( `asynchronous tasks`), integrated Dubbo (`with official starter`), MongoDB (`document database`), neo4j (`graph database`), docker (`container`), `JPA Multi-Datasource`, `Mybatis Multi-Datasource`, `code generator`', GrayLog (`log collection`), JustAuth (`third-party login`), LDAP(`CURD`), `Dynamically add/switch datasources`, Standalone RateLimiting(`AOP + Guava RateLimiter`), Distributed Ratelimiting(`AOP + Redis + Lua`), ElasticSearch 7.x(`use official Rest High Level Client`), HTTPS, Flyway(`initialize databases`).
+This project has integrated actuator (`monitoring`), admin (`visual monitoring`), logback (`log`), aopLog (`recording web request logs through AOP`), global exception handling (`json level and page level` ), freemarker (`template engine`), thymeleaf (`template engine`), Beetl (`template engine`), Enjoy (`template engine`), JdbcTemplate (`general JDBC operate database`), JPA (`powerful ORM framework `), mybatis (`powerful ORM framework`), Generic Mapper (`mybatis quick operation `), PageHelper (`powerful mybatis pagination plugin`), mybatis-plus (`mybatis quick operation`), BeetlSQL (`powerful ORM framework `), upload (`local file upload and qiniu cloud file upload`), redis (`cache`), ehcache (`cache`), email (`send various types of mail`), task (`basic scheduled tasks`), quartz (`dynamic management scheduled tasks`), xxl-job (`distributed scheduled tasks`), swagger (`API interface management and tests`), security (`RBAC-based Dynamic Rights Authentication`), SpringSession (`session sharing`), Zookeeper (`implement distributed locks by AOP`), RabbitMQ (`message queue`), Kafka (`message queue`), websocket (` server pushes the monitoring server status to front end `), socket.io (`chat room`), ureport2 (`Chinese-style report`), packaged into a `war` file, integrate ElasticSearch (`basic operations and advanced queries`), Async ( `asynchronous tasks`), integrated Dubbo (`with official starter`), MongoDB (`document database`), neo4j (`graph database`), docker (`container`), `JPA Multi-Datasource`, `Mybatis Multi-Datasource`, `code generator`', GrayLog (`log collection`), JustAuth (`third-party login`), LDAP(`CURD`), `Dynamically add/switch datasources`, Standalone RateLimiting(`AOP + Guava RateLimiter`), Distributed Ratelimiting(`AOP + Redis + Lua`), ElasticSearch 7.x(`use official Rest High Level Client`), HTTPS, Flyway(`initialize databases`),UReport2(`Chinese complex report `).
> If you have demos to contribute or needs to meet, it is very welcome to submit a [issue](https://github.com/xkcoding/spring-boot-demo/issues/new) and I will add it to my [TODO](./TODO.en.md) list.
-## Thanks
-
-- **Thanks JetBrains Offer Open Source Free License**
-- [Thanks MyBatisCodeHelper-Pro(The Best Code Generator Plugin) Offer Permanent Activation Code](https://gejun123456.github.io/MyBatisCodeHelper-Pro/#/?id=mybatiscodehelper-pro)
-
## Branch Introduction
- branch master: Based on Spring Boot version `2.1.0.RELEASE`. Every module's parent dependency is the pom.xml at root directory in convenience of managing common dependencies and learning spring boot.
@@ -55,318 +50,12 @@ This project has integrated actuator (`monitoring`), admin (`visual monitoring`)
6. **`Note: Each demo has a detailed README file. Remember to check it before running the demo~`**
7. **`Note: In some condition you have to execute sql to prepare data before running demo, don't forget it~`**
-## TODO
-
-View the [TODO](./TODO.en.md) file
-
-## Introduction of each Module
-
-| Module Name | Module Description |
-| ------------------------------------------------------------ | ------------------------------------------------------------ |
-| [spring-boot-demo-helloworld](./spring-boot-demo-helloworld) | a helloworld demo. |
-| [spring-boot-demo-properties](./spring-boot-demo-properties) | a demo to read the contents of configuration file. |
-| [spring-boot-demo-actuator](./spring-boot-demo-actuator) | a demo to integrate spring-boot-starter-actuator for monitoring the starting status and the running status of application. |
-| [spring-boot-demo-admin-client](./spring-boot-demo-admin/spring-boot-demo-admin-client) | a client demo to integrate spring-boot-admin for visually monitoring the running status of application, it can be used with spring-boot-starter-actuator. |
-| [spring-boot-demo-admin-server](./spring-boot-demo-admin/spring-boot-demo-admin-server) | a server demo to integrate spring-boot-admin for visually monitoring the running status of the spring-boot program, it can be used with spring-boot-starter-actuator. |
-| [spring-boot-demo-logback](./spring-boot-demo-logback) | a demo to integrate the logback for logging. |
-| [spring-boot-demo-log-aop](./spring-boot-demo-log-aop) | a demo to record web request logs using AOP aspect. |
-| [spring-boot-demo-exception-handler](./spring-boot-demo-exception-handler) | a demo to demonstrate global exception handling, including 2 types, the first one returns json data, and the second one jumps to error page. |
-| [spring-boot-demo-template-freemarker](./spring-boot-demo-template-freemarker) | a demo to integrate Freemarker template engine. |
-| [spring-boot-demo-template-thymeleaf](./spring-boot-demo-template-thymeleaf) | a demo to integrate Thymeleaf template engine. |
-| [spring-boot-demo-template-beetl](./spring-boot-demo-template-beetl) | a demo to integrate Beetl template engine. |
-| [spring-boot-demo-template-enjoy](./spring-boot-demo-template-enjoy) | a demo to integrate Enjoy template engine. |
-| [spring-boot-demo-orm-jdbctemplate](./spring-boot-demo-orm-jdbctemplate) | a demo to integrate the Jdbc Template for operating database and easily encapsulate the generic Dao layer. |
-| [spring-boot-demo-orm-jpa](./spring-boot-demo-orm-jpa) | a demo to integrate spring-boot-starter-data-jpa for operating database. |
-| [spring-boot-demo-orm-mybatis](./spring-boot-demo-orm-mybatis) | a demo to integrate native mybatis by using [mybatis-spring-boot-starter](https://github.com/mybatis/spring-boot-starter) dependency. |
-| [spring-boot-demo-orm-mybatis-mapper-page](./spring-boot-demo-orm-mybatis-mapper-page) | a demo to integrate [Mapper](https://github.com/abel533/Mapper) and [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) by using [mapper-spring-boot-starter](https://github.com/abel533/Mapper/tree/master/spring-boot-starter) and [pagehelper-spring-boot-starter](https://github.com/pagehelper/pagehelper-spring-boot) dependencies. |
-| [spring-boot-demo-orm-mybatis-plus](./spring-boot-demo-orm-mybatis-plus) | a demo to integrate [mybatis-plus](https://mybatis.plus/en/) by using [mybatis-plus-boot-starter](http://mp.baomidou.com/) dependency, integrate BaseMapper / BaseService / ActiveRecord to operate database. |
-| [spring-boot-demo-orm-beetlsql](./spring-boot-demo-orm-beetlsql) | a demo to integrate [beetl-sql](http://ibeetl.com/guide/#beetlsql) by using [beetl-framework-starter](http://ibeetl.com/guide/#beetlsql) dependency. |
-| [spring-boot-demo-upload](./spring-boot-demo-upload) | a file upload demo, including local file upload and qiniu cloud file upload. |
-| [spring-boot-demo-cache-redis](./spring-boot-demo-cache-redis) | a demo to integrate redis, operate data in redis, and use redis to cache data. |
-| [spring-boot-demo-cache-ehcache](./spring-boot-demo-cache-ehcache) | a demo to integrate ehcache, and use ehcache to cache data. |
-| [spring-boot-demo-email](./spring-boot-demo-email) | a demo to integrate email, including sending simple text email, HTML email (including template HTML email), attachment email, and static resource email. |
-| [spring-boot-demo-task](./spring-boot-demo-task) | a demo to show easy to use scheduled task. |
-| [spring-boot-demo-task-quartz](./spring-boot-demo-task-quartz) | a demo to integrate quartz for managing scheduled tasks, including adding new scheduled tasks, deleting scheduled tasks, suspending scheduled tasks, restoring scheduled tasks, modifying scheduled task startup times, and timing task list queries, and `providing front-end pages`. |
-| [spring-boot-demo-task-xxl-job](./spring-boot-demo-task-xxl-job) | a demo to integrate [xxl-job](http://www.xuxueli.com/xxl-job/en/#/) for distributed scheduled tasks and provide methods to manage scheduled tasks bypass `xxl-job-admin`, including scheduled task lists, trigger lists, new scheduled tasks, deleted scheduled tasks, stopped scheduled tasks, and started scheduled tasks. Modify the scheduled task and manually trigger the scheduled task. |
-| [spring-boot-demo-swagger](./spring-boot-demo-swagger) | a demo to integrate native `swagger` to manage and test API interfaces. |
-| [spring-boot-demo-swagger-beauty](./spring-boot-demo-swagger-beauty) | a demo to integrate third part of swagger dependency [swagger-bootstrap-ui](https://github.com/xiaoymin/Swagger-Bootstrap-UI) to beautify document style and manage and test API interfaces. |
-| [spring-boot-demo-rbac-security](./spring-boot-demo-rbac-security) | a demo to integrate spring security implement privilege management based on RBAC privilege model, supports custom filtering request, dynamic privilege authentication, uses JWT security authentication, supports online population statistics, manually kicks out users, etc. |
-| [spring-boot-demo-rbac-shiro](./spring-boot-demo-rbac-shiro) | NOT FINISHED YET! a demo to integrate shiro for authentication management. |
-| [spring-boot-demo-session](./spring-boot-demo-session) | a demo to integrate Spring Session to implement Session sharing, restart program Session does not expire. |
-| [spring-boot-demo-oauth](./spring-boot-demo-oauth) | NOT FINISHED YET! a demo to implement the oauth server and to implement oauth2 protocol such as the authorization code, access token. |
-| [spring-boot-demo-social](./spring-boot-demo-social) | a demo to integrate third-party login by using `justauth-spring-boot-starter` dependency to achieve QQ login, GitHub login, WeChat login, Google login, Microsoft login, Xiaomi login, enterprise WeChat login. |
-| [spring-boot-demo-zookeeper](./spring-boot-demo-zookeeper) | a demo to integrate Zookeeper and AOP to implement distributed lock. |
-| [spring-boot-demo-mq-rabbitmq](./spring-boot-demo-mq-rabbitmq) | a demo to integrate RabbitMQ implementation for message delivery and reception based on direct queue mode, fanout mode, topic mode, delay queue. |
-| [spring-boot-demo-mq-rocketmq](./spring-boot-demo-mq-rocketmq) | NOT FINISHED YET! a demo to integrate RocketMQ implementation for message delivery and reception. |
-| [spring-boot-demo-mq-kafka](./spring-boot-demo-mq-kafka) | a demo to integrate Kafka implementation for message delivery and reception. |
-| [spring-boot-demo-websocket](./spring-boot-demo-websocket) | a demo to integrate websocket, the backend actively pushes the server running status to front end. |
-| [spring-boot-demo-websocket-socketio](./spring-boot-demo-websocket-socketio) | a demo to integrate websocket by using `netty-socketio`, implement a simple chat room. |
-| [spring-boot-demo-ureport2](./spring-boot-demo-ureport2) | NOT FINISHED YET! a demo to integrate [ureport2](https://github.com/youseries/ureport) to implement complex, customized Chinese-style reports. |
-| [spring-boot-demo-uflo](./spring-boot-demo-uflo) | NOT FINISHED YET! a demo to integrate [uflo](https://github.com/youseries/uflo)(process engine like Activiti and Flowable) to quickly implement a lightweight process engine. |
-| [spring-boot-demo-urule](./spring-boot-demo-urule) | NOT FINISHED YET! a demo to integrate [urule](https://github.com/youseries/urule)(rule engine like drools) fast implementation rule engine. |
-| [spring-boot-demo-activiti](./spring-boot-demo-activiti) | NOT FINISHED YET! a demo to integrate Activiti 7 process engine. |
-| [spring-boot-demo-async](./spring-boot-demo-async) | asynchronous execution of tasks by using natively provided asynchronous task support. |
-| [spring-boot-demo-war](./spring-boot-demo-war) | packaged into a war format configuration |
-| [spring-boot-demo-elasticsearch](./spring-boot-demo-elasticsearch) | a demo to integrate ElasticSearch by using `spring-boot-starter-data-elasticsearch` to implement advanced techniques for using ElasticSearch, including creating indexes, configuring mappings, deleting indexes, adding and deleting basic operations, complex queries, advanced queries, aggregate queries, etc. |
-| [spring-boot-demo-dubbo](./spring-boot-demo-dubbo) | a demo to integrate Dubbo, common module `spring-boot-demo-dubbo-common`, service provider `spring-boot-demo-dubbo-provider`, service consumer `spring-boot-demo-dubbo-consumer`. |
-| [spring-boot-demo-mongodb](./spring-boot-demo-mongodb) | a demo to integrate MongoDB and use the official starter to CRUD. |
-| [spring-boot-demo-neo4j](./spring-boot-demo-neo4j) | a demo to integrate Neo4j graph database to implement a campus character relationship network. |
-| [spring-boot-demo-docker](./spring-boot-demo-docker) | docker container. |
-| [spring-boot-demo-multi-datasource-jpa](./spring-boot-demo-multi-datasource-jpa) | a demo to implement JPA multi-datasource. |
-| [spring-boot-demo-multi-datasource-mybatis](./spring-boot-demo-multi-datasource-mybatis) | a demo to implement Mybatis multi-datasource by using an open source solution from Mybatis-Plus. |
-| [spring-boot-demo-sharding-jdbc](./spring-boot-demo-sharding-jdbc) | a demo to use `sharding-jdbc` to implement sub-database and sub-tables, while ORM uses Mybatis-Plus. |
-| [spring-boot-demo-tio](./spring-boot-demo-tio) | NOT FINISHED YET! a demo to integrate t-io(a network programming framework like netty). |
-| [spring-boot-demo-grpc](./spring-boot-demo-grpc) | NOT FINISHED YET! a demo to integrate Google grpc, need to be configure tls/ssl, see [ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5). |
-| [spring-boot-demo-codegen](./spring-boot-demo-codegen) | a demo to integrate velocity template engine to implement code generator, improve development efficiency. |
-| [spring-boot-demo-graylog](./spring-boot-demo-graylog) | a demo to integrate graylog for unified log collection. |
-| spring-boot-demo-sso | NOT FINISHED YET! a demo to integrate Single Sign On, see [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12). |
-| [spring-boot-demo-ldap](./spring-boot-demo-ldap) | a demo to integrate LDAP to use `spring-boot-starter-data-ldap` to implement CURD operations and give the login demo, see [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23), thanks [@fxbin](https://github.com/fxbin). |
-| [spring-boot-demo-dynamic-datasource](./spring-boot-demo-dynamic-datasource) | a demo to add datasource dynamically, switch datasource dynamically. |
-| [spring-boot-demo-ratelimit-guava](./spring-boot-demo-ratelimit-guava) | a demo to use use Guava RateLimiter to protect API by standalone rate limiting. |
-| [spring-boot-demo-ratelimit-redis](./spring-boot-demo-ratelimit-redis) | a demo to use Redis and Lua script implementation to protect API by distributed rate limiting. |
-| [spring-boot-demo-https](./spring-boot-demo-https) | a demo to integrate HTTPS. |
-| [spring-boot-demo-elasticsearch-rest-high-level-client](./spring-boot-demo-elasticsearch-rest-high-level-client) | a demo to integrate ElasticSearch 7.x version by using official Rest High Level Client to operate ES data. |
-| [spring-boot-demo-flyway](./spring-boot-flyway) | a demo to integrate Flyway to initialize tables and data in database, Flyway also support the sql script version control. |
-
-## License
-
-[MIT](http://opensource.org/licenses/MIT)
-
-Copyright (c) 2018 Yangkai.Shen
-
## Stargazers over time
[![Stargazers over time](https://starchart.cc/xkcoding/spring-boot-demo.svg)](https://starchart.cc/xkcoding/spring-boot-demo)
## Appendix
-### Pom.xml in the root directory
-
-```xml
-
-
-
- 4.0.0
-
- com.xkcoding
- spring-boot-demo
- 1.0.0-SNAPSHOT
-
- spring-boot-demo-helloworld
- spring-boot-demo-properties
- spring-boot-demo-actuator
- spring-boot-demo-admin
- spring-boot-demo-logback
- spring-boot-demo-log-aop
- spring-boot-demo-exception-handler
- spring-boot-demo-template-freemarker
- spring-boot-demo-template-thymeleaf
- spring-boot-demo-template-beetl
- spring-boot-demo-template-enjoy
- spring-boot-demo-orm-jdbctemplate
- spring-boot-demo-orm-jpa
- spring-boot-demo-orm-mybatis
- spring-boot-demo-orm-mybatis-mapper-page
- spring-boot-demo-orm-mybatis-plus
- spring-boot-demo-orm-beetlsql
- spring-boot-demo-upload
- spring-boot-demo-cache-redis
- spring-boot-demo-cache-ehcache
- spring-boot-demo-email
- spring-boot-demo-task
- spring-boot-demo-task-quartz
- spring-boot-demo-task-xxl-job
- spring-boot-demo-swagger
- spring-boot-demo-swagger-beauty
- spring-boot-demo-rbac-security
- spring-boot-demo-rbac-shiro
- spring-boot-demo-session
- spring-boot-demo-oauth
- spring-boot-demo-social
- spring-boot-demo-zookeeper
- spring-boot-demo-mq-rabbitmq
- spring-boot-demo-mq-rocketmq
- spring-boot-demo-mq-kafka
- spring-boot-demo-websocket
- spring-boot-demo-websocket-socketio
- spring-boot-demo-ureport2
- spring-boot-demo-uflo
- spring-boot-demo-urule
- spring-boot-demo-activiti
- spring-boot-demo-async
- spring-boot-demo-dubbo
- spring-boot-demo-war
- spring-boot-demo-elasticsearch
- spring-boot-demo-mongodb
- spring-boot-demo-neo4j
- spring-boot-demo-docker
- spring-boot-demo-multi-datasource-jpa
- spring-boot-demo-multi-datasource-mybatis
- spring-boot-demo-sharding-jdbc
- spring-boot-demo-tio
- spring-boot-demo-codegen
- spring-boot-demo-graylog
- spring-boot-demo-ldap
- spring-boot-demo-dynamic-datasource
- spring-boot-demo-ratelimit-guava
- spring-boot-demo-ratelimit-redis
- spring-boot-demo-elasticsearch-rest-high-level-client
- spring-boot-demo-https
- spring-boot-demo-flyway
-
- pom
-
- spring-boot-demo
- http://xkcoding.com
-
-
- UTF-8
- UTF-8
- 1.8
- 1.8
- 1.8
- 2.1.0.RELEASE
- 8.0.12
- 5.0.0
- 28.1-jre
- 1.20
-
-
-
-
- aliyun
- aliyun
- https://maven.aliyun.com/repository/public
-
- true
-
-
- false
-
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-dependencies
- ${spring.boot.version}
- pom
- import
-
-
- mysql
- mysql-connector-java
- ${mysql.version}
-
-
-
- cn.hutool
- hutool-all
- ${hutool.version}
-
-
-
- com.google.guava
- guava
- ${guava.version}
-
-
-
- eu.bitwalker
- UserAgentUtils
- ${user.agent.version}
-
-
-
-
-
-
-
-
- maven-clean-plugin
- 3.0.0
-
-
- maven-resources-plugin
- 3.0.2
-
-
- maven-compiler-plugin
- 3.7.0
-
-
- maven-surefire-plugin
- 2.20.1
-
-
- maven-jar-plugin
- 3.0.2
-
-
- maven-install-plugin
- 2.5.2
-
-
- maven-deploy-plugin
- 2.8.2
-
-
- org.springframework.boot
- spring-boot-maven-plugin
- ${spring.boot.version}
-
-
-
- repackage
-
-
-
-
-
-
-
-
-```
-
-### Official starter introduction
-
-| Name | Description |
-| :------------------------------------- | :----------------------------------------------------------- |
-| spring-boot-starter | The core Spring Boot starter, including auto-configuration support, logging and YAML. |
-| spring-boot-starter-actuator | Production ready features to help you monitor and manage your application. |
-| spring-boot-starter-amqp | Support for RabbitMQ messages |
-| spring-boot-starter-aop | Support for aspect-oriented programming including spring-aop and AspectJ. |
-| spring-boot-starter-batch | Support for “Spring Batch” including HSQLDB database. |
-| spring-boot-starter-cache | Support for Spring’s Cache abstraction. |
-| spring-boot-starter-data-elasticsearch | Support for the Elasticsearch search and analytics engine including spring-data-elasticsearch. |
-| spring-boot-starter-data-jpa | Support for the “Java Persistence API” including spring-data-jpa, spring-orm and Hibernate. |
-| spring-boot-starter-data-mongodb | Support for the MongoDB NoSQL Database, including spring-data-mongodb. |
-| spring-boot-starter-data-rest | Support for exposing Spring Data repositories over REST via spring-data-rest-webmvc. |
-| spring-boot-starter-data-solr | Support for the Apache Solr search platform, including spring-data-solr. |
-| spring-boot-starter-freemarker | Support for the FreeMarker templating engine. |
-| spring-boot-starter-groovy-templates | Support for the Groovy templating engine. |
-| spring-boot-starter-integration | Support for common spring-integration modules. |
-| spring-boot-starter-jdbc | Support for JDBC databases. |
-| spring-boot-starter-jersey | Support for the Jersey RESTful Web Services framework. |
-| spring-boot-starter-jta-atomikos | Support for JTA distributed transactions via Atomikos. |
-| spring-boot-starter-jta-bitronix | Support for JTA distributed transactions via Bitronix. |
-| spring-boot-starter-mail | Support for javax.mail. |
-| spring-boot-starter-mustache | Support for the Mustache templating engine. |
-| spring-boot-starter-redis | Support for the REDIS key-value data store, including spring-redis. |
-| spring-boot-starter-security | Support for spring-security. |
-| spring-boot-starter-social-facebook | Support for spring-social-facebook. |
-| spring-boot-starter-social-linkedin | Support for spring-social-linkedin. |
-| spring-boot-starter-social-twitter | Support for spring-social-twitter. |
-| spring-boot-starter-test | Support for common test dependencies, including JUnit, Hamcrest and Mockito along with the spring-test module. |
-| spring-boot-starter-thymeleaf | Support for the Thymeleaf templating engine, including integration with Spring. |
-| spring-boot-starter-velocity | Support for the Velocity templating engine. |
-| spring-boot-starter-web | Support for full-stack web development, including Tomcat and spring-webmvc. |
-| spring-boot-starter-websocket | Support for WebSocket development. |
-| spring-boot-starter-ws | Support for Spring Web Services. |
-
### Recommended Open source
- `JustAuth`:The most comprehensive open source library for third-party logins in history,https://github.com/justauth/JustAuth
@@ -375,6 +64,87 @@ Copyright (c) 2018 Yangkai.Shen
- `SpringBlade`:Complete micro-service online solution (required for enterprise development),https://github.com/chillzhuang/SpringBlade
- `Pig`:The universe's strongest micro-service certification authorized scaffolding (architect necessary),https://github.com/pigxcloud/pig
-### Advertisement
+### TODO
+
+View the [TODO](./TODO.en.md) file
+
+### Introduction of each Module
-[![JD_CLOUD](assets/jdcloud.jpg)](https://re.jdcloud.com/cps?returnUrl=aHR0cHM6Ly93d3cuamRjbG91ZC5jb20vY24vYWN0aXZpdHkveWVhci1lbmQ_bUlkPTE4JmNwc0tleT1iMjg2Y2Q0ZmExMWM0ODZhODU2NmUwNjc5MGQ0MzY4MA==)
+| Module Name | Module Description |
+| ------------------------------------------------------------ | ------------------------------------------------------------ |
+| [demo-helloworld](./demo-helloworld) | a helloworld demo. |
+| [demo-properties](./demo-properties) | a demo to read the contents of configuration file. |
+| [demo-actuator](./demo-actuator) | a demo to integrate spring-boot-starter-actuator for monitoring the starting status and the running status of application. |
+| [demo-admin-client](./demo-admin/admin-client) | a client demo to integrate spring-boot-admin for visually monitoring the running status of application, it can be used with spring-boot-starter-actuator. |
+| [demo-admin-server](./demo-admin/admin-server) | a server demo to integrate spring-boot-admin for visually monitoring the running status of the spring-boot program, it can be used with spring-boot-starter-actuator. |
+| [demo-logback](./demo-logback) | a demo to integrate the logback for logging. |
+| [demo-log-aop](./demo-log-aop) | a demo to record web request logs using AOP aspect. |
+| [demo-exception-handler](./demo-exception-handler) | a demo to demonstrate global exception handling, including 2 types, the first one returns json data, and the second one jumps to error page. |
+| [demo-template-freemarker](./demo-template-freemarker) | a demo to integrate Freemarker template engine. |
+| [demo-template-thymeleaf](./demo-template-thymeleaf) | a demo to integrate Thymeleaf template engine. |
+| [demo-template-beetl](./demo-template-beetl) | a demo to integrate Beetl template engine. |
+| [demo-template-enjoy](./demo-template-enjoy) | a demo to integrate Enjoy template engine. |
+| [demo-orm-jdbctemplate](./demo-orm-jdbctemplate) | a demo to integrate the Jdbc Template for operating database and easily encapsulate the generic Dao layer. |
+| [demo-orm-jpa](./demo-orm-jpa) | a demo to integrate spring-boot-starter-data-jpa for operating database. |
+| [demo-orm-mybatis](./demo-orm-mybatis) | a demo to integrate native mybatis by using [mybatis-spring-boot-starter](https://github.com/mybatis/spring-boot-starter) dependency. |
+| [demo-orm-mybatis-mapper-page](./demo-orm-mybatis-mapper-page) | a demo to integrate [Mapper](https://github.com/abel533/Mapper) and [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) by using [mapper-spring-boot-starter](https://github.com/abel533/Mapper/tree/master/spring-boot-starter) and [pagehelper-spring-boot-starter](https://github.com/pagehelper/pagehelper-spring-boot) dependencies. |
+| [demo-orm-mybatis-plus](./demo-orm-mybatis-plus) | a demo to integrate [mybatis-plus](https://mybatis.plus/en/) by using [mybatis-plus-boot-starter](http://mp.baomidou.com/) dependency, integrate BaseMapper / BaseService / ActiveRecord to operate database. |
+| [demo-orm-beetlsql](./demo-orm-beetlsql) | a demo to integrate [beetl-sql](http://ibeetl.com/guide/#beetlsql) by using [beetl-framework-starter](http://ibeetl.com/guide/#beetlsql) dependency. |
+| [demo-upload](./demo-upload) | a file upload demo, including local file upload and qiniu cloud file upload. |
+| [demo-cache-redis](./demo-cache-redis) | a demo to integrate redis, operate data in redis, and use redis to cache data. |
+| [demo-cache-ehcache](./demo-cache-ehcache) | a demo to integrate ehcache, and use ehcache to cache data. |
+| [demo-email](./demo-email) | a demo to integrate email, including sending simple text email, HTML email (including template HTML email), attachment email, and static resource email. |
+| [demo-task](./demo-task) | a demo to show easy to use scheduled task. |
+| [demo-task-quartz](./demo-task-quartz) | a demo to integrate quartz for managing scheduled tasks, including adding new scheduled tasks, deleting scheduled tasks, suspending scheduled tasks, restoring scheduled tasks, modifying scheduled task startup times, and timing task list queries, and `providing front-end pages`. |
+| [demo-task-xxl-job](./demo-task-xxl-job) | a demo to integrate [xxl-job](http://www.xuxueli.com/xxl-job/en/#/) for distributed scheduled tasks and provide methods to manage scheduled tasks bypass `xxl-job-admin`, including scheduled task lists, trigger lists, new scheduled tasks, deleted scheduled tasks, stopped scheduled tasks, and started scheduled tasks. Modify the scheduled task and manually trigger the scheduled task. |
+| [demo-swagger](./demo-swagger) | a demo to integrate native `swagger` to manage and test API interfaces. |
+| [demo-swagger-beauty](./demo-swagger-beauty) | a demo to integrate third part of swagger dependency [swagger-bootstrap-ui](https://github.com/xiaoymin/Swagger-Bootstrap-UI) to beautify document style and manage and test API interfaces. |
+| [demo-rbac-security](./demo-rbac-security) | a demo to integrate spring security implement privilege management based on RBAC privilege model, supports custom filtering request, dynamic privilege authentication, uses JWT security authentication, supports online population statistics, manually kicks out users, etc. |
+| [demo-rbac-shiro](./demo-rbac-shiro) | NOT FINISHED YET! a demo to integrate shiro for authentication management. |
+| [demo-session](./demo-session) | a demo to integrate Spring Session to implement Session sharing, restart program Session does not expire. |
+| [demo-oauth](./demo-oauth) | NOT FINISHED YET! a demo to implement the oauth server and to implement oauth2 protocol such as the authorization code, access token. |
+| [demo-social](./demo-social) | a demo to integrate third-party login by using `justauth-spring-boot-starter` dependency to achieve QQ login, GitHub login, WeChat login, Google login, Microsoft login, Xiaomi login, enterprise WeChat login. |
+| [demo-zookeeper](./demo-zookeeper) | a demo to integrate Zookeeper and AOP to implement distributed lock. |
+| [demo-mq-rabbitmq](./demo-mq-rabbitmq) | a demo to integrate RabbitMQ implementation for message delivery and reception based on direct queue mode, fanout mode, topic mode, delay queue. |
+| [demo-mq-rocketmq](./demo-mq-rocketmq) | NOT FINISHED YET! a demo to integrate RocketMQ implementation for message delivery and reception. |
+| [demo-mq-kafka](./demo-mq-kafka) | a demo to integrate Kafka implementation for message delivery and reception. |
+| [demo-websocket](./demo-websocket) | a demo to integrate websocket, the backend actively pushes the server running status to front end. |
+| [demo-websocket-socketio](./demo-websocket-socketio) | a demo to integrate websocket by using `netty-socketio`, implement a simple chat room. |
+| [demo-ureport2](./demo-ureport2) | NOT FINISHED YET! a demo to integrate [ureport2](https://github.com/youseries/ureport) to implement complex, customized Chinese-style reports. |
+| [demo-uflo](./demo-uflo) | NOT FINISHED YET! a demo to integrate [uflo](https://github.com/youseries/uflo)(process engine like Activiti and Flowable) to quickly implement a lightweight process engine. |
+| [demo-urule](./demo-urule) | NOT FINISHED YET! a demo to integrate [urule](https://github.com/youseries/urule)(rule engine like drools) fast implementation rule engine. |
+| [demo-activiti](./demo-activiti) | NOT FINISHED YET! a demo to integrate Activiti 7 process engine. |
+| [demo-async](./demo-async) | asynchronous execution of tasks by using natively provided asynchronous task support. |
+| [demo-war](./demo-war) | packaged into a war format configuration |
+| [demo-elasticsearch](./demo-elasticsearch) | a demo to integrate ElasticSearch by using `spring-boot-starter-data-elasticsearch` to implement advanced techniques for using ElasticSearch, including creating indexes, configuring mappings, deleting indexes, adding and deleting basic operations, complex queries, advanced queries, aggregate queries, etc. |
+| [demo-dubbo](./demo-dubbo) | a demo to integrate Dubbo, common module `spring-boot-demo-dubbo-common`, service provider `spring-boot-demo-dubbo-provider`, service consumer `spring-boot-demo-dubbo-consumer`. |
+| [demo-mongodb](./demo-mongodb) | a demo to integrate MongoDB and use the official starter to CRUD. |
+| [demo-neo4j](./demo-neo4j) | a demo to integrate Neo4j graph database to implement a campus character relationship network. |
+| [demo-docker](./demo-docker) | docker container. |
+| [demo-multi-datasource-jpa](./demo-multi-datasource-jpa) | a demo to implement JPA multi-datasource. |
+| [demo-multi-datasource-mybatis](./demo-multi-datasource-mybatis) | a demo to implement Mybatis multi-datasource by using an open source solution from Mybatis-Plus. |
+| [demo-sharding-jdbc](./demo-sharding-jdbc) | a demo to use `sharding-jdbc` to implement sub-database and sub-tables, while ORM uses Mybatis-Plus. |
+| [demo-tio](./demo-tio) | NOT FINISHED YET! a demo to integrate t-io(a network programming framework like netty). |
+| demo-grpc | NOT FINISHED YET! a demo to integrate Google grpc, need to be configure tls/ssl, see [ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5). |
+| [demo-codegen](./demo-codegen) | a demo to integrate velocity template engine to implement code generator, improve development efficiency. |
+| [demo-graylog](./demo-graylog) | a demo to integrate graylog for unified log collection. |
+| demo-sso | NOT FINISHED YET! a demo to integrate Single Sign On, see [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12). |
+| [demo-ldap](./demo-ldap) | a demo to integrate LDAP to use `spring-boot-starter-data-ldap` to implement CURD operations and give the login demo, see [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23), thanks [@fxbin](https://github.com/fxbin). |
+| [demo-dynamic-datasource](./demo-dynamic-datasource) | a demo to add datasource dynamically, switch datasource dynamically. |
+| [demo-ratelimit-guava](./demo-ratelimit-guava) | a demo to use use Guava RateLimiter to protect API by standalone rate limiting. |
+| [demo-ratelimit-redis](./demo-ratelimit-redis) | a demo to use Redis and Lua script implementation to protect API by distributed rate limiting. |
+| [demo-https](./demo-https) | a demo to integrate HTTPS. |
+| [demo-elasticsearch-rest-high-level-client](./demo-elasticsearch-rest-high-level-client) | a demo to integrate ElasticSearch 7.x version by using official Rest High Level Client to operate ES data. |
+| [demo-flyway](./demo-flyway) | a demo to integrate Flyway to initialize tables and data in database, Flyway also support the sql script version control. |
+| [demo-ureport2](./demo-ureport2) | a demo to integrate Ureport2 to design the Chinese complex report file. |
+
+### Thanks
+
+- **Thanks JetBrains Offer Open Source Free License**
+- [Thanks MyBatisCodeHelper-Pro(The Best Code Generator Plugin) Offer Permanent Activation Code](https://gejun123456.github.io/MyBatisCodeHelper-Pro/#/?id=mybatiscodehelper-pro)
+
+### License
+
+[MIT](http://opensource.org/licenses/MIT)
+
+Copyright (c) 2018 Yangkai.Shen
diff --git a/README.md b/README.md
index c6c493f..2d9b99a 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -20,18 +20,12 @@
## 项目简介
-`spring boot demo` 是一个用来深度学习并实战 `spring boot` 的项目,目前总共包含 **`66`** 个集成demo,已经完成 **`54`** 个。
+`spring boot demo` 是一个用来深度学习并实战 `spring boot` 的项目,目前总共包含 **`66`** 个集成demo,已经完成 **`55`** 个。
-该项目已成功集成 actuator(`监控`)、admin(`可视化监控`)、logback(`日志`)、aopLog(`通过AOP记录web请求日志`)、统一异常处理(`json级别和页面级别`)、freemarker(`模板引擎`)、thymeleaf(`模板引擎`)、Beetl(`模板引擎`)、Enjoy(`模板引擎`)、JdbcTemplate(`通用JDBC操作数据库`)、JPA(`强大的ORM框架`)、mybatis(`强大的ORM框架`)、通用Mapper(`快速操作Mybatis`)、PageHelper(`通用的Mybatis分页插件`)、mybatis-plus(`快速操作Mybatis`)、BeetlSQL(`强大的ORM框架`)、upload(`本地文件上传和七牛云文件上传`)、redis(`缓存`)、ehcache(`缓存`)、email(`发送各种类型邮件`)、task(`基础定时任务`)、quartz(`动态管理定时任务`)、xxl-job(`分布式定时任务`)、swagger(`API接口管理测试`)、security(`基于RBAC的动态权限认证`)、SpringSession(`Session共享`)、Zookeeper(`结合AOP实现分布式锁`)、RabbitMQ(`消息队列`)、Kafka(`消息队列`)、websocket(`服务端推送监控服务器运行信息`)、socket.io(`聊天室`)、ureport2(`中国式报表`)、打包成`war`文件、集成 ElasticSearch(`基本操作和高级查询`)、Async(`异步任务`)、集成Dubbo(`采用官方的starter`)、MongoDB(`文档数据库`)、neo4j(`图数据库`)、docker(`容器化`)、`JPA多数据源`、`Mybatis多数据源`、`代码生成器`、GrayLog(`日志收集`)、JustAuth(`第三方登录`)、LDAP(`增删改查`)、`动态添加/切换数据源`、单机限流(`AOP + Guava RateLimiter`)、分布式限流(`AOP + Redis + Lua`)、ElasticSearch 7.x(`使用官方 Rest High Level Client`)、HTTPS、Flyway(`数据库初始化`)。
+该项目已成功集成 actuator(`监控`)、admin(`可视化监控`)、logback(`日志`)、aopLog(`通过AOP记录web请求日志`)、统一异常处理(`json级别和页面级别`)、freemarker(`模板引擎`)、thymeleaf(`模板引擎`)、Beetl(`模板引擎`)、Enjoy(`模板引擎`)、JdbcTemplate(`通用JDBC操作数据库`)、JPA(`强大的ORM框架`)、mybatis(`强大的ORM框架`)、通用Mapper(`快速操作Mybatis`)、PageHelper(`通用的Mybatis分页插件`)、mybatis-plus(`快速操作Mybatis`)、BeetlSQL(`强大的ORM框架`)、upload(`本地文件上传和七牛云文件上传`)、redis(`缓存`)、ehcache(`缓存`)、email(`发送各种类型邮件`)、task(`基础定时任务`)、quartz(`动态管理定时任务`)、xxl-job(`分布式定时任务`)、swagger(`API接口管理测试`)、security(`基于RBAC的动态权限认证`)、SpringSession(`Session共享`)、Zookeeper(`结合AOP实现分布式锁`)、RabbitMQ(`消息队列`)、Kafka(`消息队列`)、websocket(`服务端推送监控服务器运行信息`)、socket.io(`聊天室`)、ureport2(`中国式报表`)、打包成`war`文件、集成 ElasticSearch(`基本操作和高级查询`)、Async(`异步任务`)、集成Dubbo(`采用官方的starter`)、MongoDB(`文档数据库`)、neo4j(`图数据库`)、docker(`容器化`)、`JPA多数据源`、`Mybatis多数据源`、`代码生成器`、GrayLog(`日志收集`)、JustAuth(`第三方登录`)、LDAP(`增删改查`)、`动态添加/切换数据源`、单机限流(`AOP + Guava RateLimiter`)、分布式限流(`AOP + Redis + Lua`)、ElasticSearch 7.x(`使用官方 Rest High Level Client`)、HTTPS、Flyway(`数据库初始化`)、UReport2(`中国式复杂报表`)。
> 如果大家还有想要集成的demo,也可在 [issue](https://github.com/xkcoding/spring-boot-demo/issues/new) 里提需求。我会额外添加在 [TODO](./TODO.md) 列表里。✊
-## 感谢
-
-- **感谢 JetBrains 提供的免费开源 License**
-
-- [感谢史上最牛的代码生成插件 MyBatisCodeHelper-Pro 提供的永久激活码](https://gejun123456.github.io/MyBatisCodeHelper-Pro/#/?id=mybatiscodehelper-pro)
-
## 分支介绍
- master 分支:基于 Spring Boot 版本 `2.1.0.RELEASE`,每个 Module 的 parent 依赖根目录下的 pom.xml,主要用于管理每个 Module 的通用依赖版本,方便大家学习。
@@ -56,322 +50,106 @@
6. **`注意:每个 demo 均有详细的 README 配套,食用 demo 前记得先看看哦~`**
7. **`注意:运行各个 demo 之前,有些是需要事先初始化数据库数据的,亲们别忘记了哦~`**
-## 开发计划
-
-查看 [TODO](./TODO.md) 文件
-
-## 各 Module 介绍
-
-| Module 名称 | Module 介绍 |
-| ------------------------------------------------------------ | ------------------------------------------------------------ |
-| [spring-boot-demo-helloworld](./spring-boot-demo-helloworld) | spring-boot 的一个 helloworld |
-| [spring-boot-demo-properties](./spring-boot-demo-properties) | spring-boot 读取配置文件中的内容 |
-| [spring-boot-demo-actuator](./spring-boot-demo-actuator) | spring-boot 集成 spring-boot-starter-actuator 用于监控 spring-boot 的启动和运行状态 |
-| [spring-boot-demo-admin-client](./spring-boot-demo-admin/spring-boot-demo-admin-client) | spring-boot 集成 spring-boot-admin 来可视化的监控 spring-boot 程序的运行状态,可以与 actuator 互相搭配使用,客户端示例 |
-| [spring-boot-demo-admin-server](./spring-boot-demo-admin/spring-boot-demo-admin-server) | spring-boot 集成 spring-boot-admin 来可视化的监控 spring-boot 程序的运行状态,可以与 actuator 互相搭配使用,服务端示例 |
-| [spring-boot-demo-logback](./spring-boot-demo-logback) | spring-boot 集成 logback 日志 |
-| [spring-boot-demo-log-aop](./spring-boot-demo-log-aop) | spring-boot 使用 AOP 切面的方式记录 web 请求日志 |
-| [spring-boot-demo-exception-handler](./spring-boot-demo-exception-handler) | spring-boot 统一异常处理,包括2种,第一种返回统一的 json 格式,第二种统一跳转到异常页面 |
-| [spring-boot-demo-template-freemarker](./spring-boot-demo-template-freemarker) | spring-boot 集成 Freemarker 模板引擎 |
-| [spring-boot-demo-template-thymeleaf](./spring-boot-demo-template-thymeleaf) | spring-boot 集成 Thymeleaf 模板引擎 |
-| [spring-boot-demo-template-beetl](./spring-boot-demo-template-beetl) | spring-boot 集成 Beetl 模板引擎 |
-| [spring-boot-demo-template-enjoy](./spring-boot-demo-template-enjoy) | spring-boot 集成 Enjoy 模板引擎 |
-| [spring-boot-demo-orm-jdbctemplate](./spring-boot-demo-orm-jdbctemplate) | spring-boot 集成 Jdbc Template 操作数据库,并简易封装通用 Dao 层 |
-| [spring-boot-demo-orm-jpa](./spring-boot-demo-orm-jpa) | spring-boot 集成 spring-boot-starter-data-jpa 操作数据库 |
-| [spring-boot-demo-orm-mybatis](./spring-boot-demo-orm-mybatis) | spring-boot 集成原生mybatis,使用 [mybatis-spring-boot-starter](https://github.com/mybatis/spring-boot-starter) 集成 |
-| [spring-boot-demo-orm-mybatis-mapper-page](./spring-boot-demo-orm-mybatis-mapper-page) | spring-boot 集成[通用Mapper](https://github.com/abel533/Mapper)和[PageHelper](https://github.com/pagehelper/Mybatis-PageHelper),使用 [mapper-spring-boot-starter](https://github.com/abel533/Mapper/tree/master/spring-boot-starter) 和 [pagehelper-spring-boot-starter](https://github.com/pagehelper/pagehelper-spring-boot) 集成 |
-| [spring-boot-demo-orm-mybatis-plus](./spring-boot-demo-orm-mybatis-plus) | spring-boot 集成 [mybatis-plus](https://mybatis.plus/),使用 [mybatis-plus-boot-starter](http://mp.baomidou.com/) 集成,集成 BaseMapper、BaseService、ActiveRecord 操作数据库 |
-| [spring-boot-demo-orm-beetlsql](./spring-boot-demo-orm-beetlsql) | spring-boot 集成 [beetl-sql](http://ibeetl.com/guide/#beetlsql),使用 [beetl-framework-starter](http://ibeetl.com/guide/#beetlsql) 集成 |
-| [spring-boot-demo-upload](./spring-boot-demo-upload) | spring-boot 文件上传示例,包含本地文件上传以及七牛云文件上传 |
-| [spring-boot-demo-cache-redis](./spring-boot-demo-cache-redis) | spring-boot 整合 redis,操作redis中的数据,并使用redis缓存数据 |
-| [spring-boot-demo-cache-ehcache](./spring-boot-demo-cache-ehcache) | spring-boot 整合 ehcache,使用 ehcache 缓存数据 |
-| [spring-boot-demo-email](./spring-boot-demo-email) | spring-boot 整合 email,包括发送简单文本邮件、HTML邮件(包括模板HTML邮件)、附件邮件、静态资源邮件 |
-| [spring-boot-demo-task](./spring-boot-demo-task) | spring-boot 快速实现定时任务 |
-| [spring-boot-demo-task-quartz](./spring-boot-demo-task-quartz) | spring-boot 整合 quartz,并实现对定时任务的管理,包括新增定时任务,删除定时任务,暂停定时任务,恢复定时任务,修改定时任务启动时间,以及定时任务列表查询,`提供前端页面` |
-| [spring-boot-demo-task-xxl-job](./spring-boot-demo-task-xxl-job) | spring-boot 整合[xxl-job](http://www.xuxueli.com/xxl-job/en/#/),并提供绕过 `xxl-job-admin` 对定时任务的管理的方法,包括定时任务列表,触发器列表,新增定时任务,删除定时任务,停止定时任务,启动定时任务,修改定时任务,手动触发定时任务 |
-| [spring-boot-demo-swagger](./spring-boot-demo-swagger) | spring-boot 集成原生的 `swagger` 用于统一管理、测试 API 接口 |
-| [spring-boot-demo-swagger-beauty](./spring-boot-demo-swagger-beauty) | spring-boot 集成第三方 `swagger` [swagger-bootstrap-ui](https://github.com/xiaoymin/Swagger-Bootstrap-UI) 美化API文档样式,用于统一管理、测试 API 接口 |
-| [spring-boot-demo-rbac-security](./spring-boot-demo-rbac-security) | spring-boot 集成 spring security 完成基于RBAC权限模型的权限管理,支持自定义过滤请求,动态权限认证,使用 JWT 安全认证,支持在线人数统计,手动踢出用户等操作 |
-| [spring-boot-demo-rbac-shiro](./spring-boot-demo-rbac-shiro) | spring-boot 集成 shiro 实现权限管理 待完成 |
-| [spring-boot-demo-session](./spring-boot-demo-session) | spring-boot 集成 Spring Session 实现Session共享、重启程序Session不失效 |
-| [spring-boot-demo-oauth](./spring-boot-demo-oauth) | spring-boot 实现 oauth 服务器功能,实现授权码机制 待完成 |
-| [spring-boot-demo-social](./spring-boot-demo-social) | spring-boot 集成第三方登录,集成 `justauth-spring-boot-starter` 实现QQ登录、GitHub登录、微信登录、谷歌登录、微软登录、小米登录、企业微信登录。 |
-| [spring-boot-demo-zookeeper](./spring-boot-demo-zookeeper) | spring-boot 集成 Zookeeper 结合AOP实现分布式锁 |
-| [spring-boot-demo-mq-rabbitmq](./spring-boot-demo-mq-rabbitmq) | spring-boot 集成 RabbitMQ 实现基于直接队列模式、分列模式、主题模式、延迟队列的消息发送和接收 |
-| [spring-boot-demo-mq-rocketmq](./spring-boot-demo-mq-rocketmq) | spring-boot 集成 RocketMQ,实现消息的发送和接收 待完成 |
-| [spring-boot-demo-mq-kafka](./spring-boot-demo-mq-kafka) | spring-boot 集成 kafka,实现消息的发送和接收 |
-| [spring-boot-demo-websocket](./spring-boot-demo-websocket) | spring-boot 集成 websocket,后端主动推送前端服务器运行信息 |
-| [spring-boot-demo-websocket-socketio](./spring-boot-demo-websocket-socketio) | spring-boot 使用 netty-socketio 集成 websocket,实现一个简单的聊天室 |
-| [spring-boot-demo-ureport2](./spring-boot-demo-ureport2) | spring-boot 集成 ureport2 实现复杂的自定义的中国式报表 待完成 |
-| [spring-boot-demo-uflo](./spring-boot-demo-uflo) | spring-boot 集成 uflo 快速实现轻量级流程引擎 待完成 |
-| [spring-boot-demo-urule](./spring-boot-demo-urule) | spring-boot 集成 urule 快速实现规则引擎 待完成 |
-| [spring-boot-demo-activiti](./spring-boot-demo-activiti) | spring-boot 集成 activiti 7 流程引擎 待完成 |
-| [spring-boot-demo-async](./spring-boot-demo-async) | spring-boot 使用原生提供的异步任务支持,实现异步执行任务 |
-| [spring-boot-demo-war](./spring-boot-demo-war) | spring-boot 打成 war 包的配置 |
-| [spring-boot-demo-elasticsearch](./spring-boot-demo-elasticsearch) | spring-boot 集成 ElasticSearch,集成 `spring-boot-starter-data-elasticsearch` 完成对 ElasticSearch 的高级使用技巧,包括创建索引、配置映射、删除索引、增删改查基本操作、复杂查询、高级查询、聚合查询等 |
-| [spring-boot-demo-dubbo](./spring-boot-demo-dubbo) | spring-boot 集成 Dubbo,分别为公共模块 `spring-boot-demo-dubbo-common`、服务提供方`spring-boot-demo-dubbo-provider`、服务调用方`spring-boot-demo-dubbo-consumer` |
-| [spring-boot-demo-mongodb](./spring-boot-demo-mongodb) | spring-boot 集成 MongoDB,使用官方的 starter 实现增删改查 |
-| [spring-boot-demo-neo4j](./spring-boot-demo-neo4j) | spring-boot 集成 Neo4j 图数据库,实现一个校园人物关系网的demo |
-| [spring-boot-demo-docker](./spring-boot-demo-docker) | spring-boot 容器化 |
-| [spring-boot-demo-multi-datasource-jpa](./spring-boot-demo-multi-datasource-jpa) | spring-boot 使用JPA集成多数据源 |
-| [spring-boot-demo-multi-datasource-mybatis](./spring-boot-demo-multi-datasource-mybatis) | spring-boot 使用Mybatis集成多数据源,使用 Mybatis-Plus 提供的开源解决方案实现 |
-| [spring-boot-demo-sharding-jdbc](./spring-boot-demo-sharding-jdbc) | spring-boot 使用 `sharding-jdbc` 实现分库分表,同时ORM采用 Mybatis-Plus |
-| [spring-boot-demo-tio](./spring-boot-demo-tio) | spring-boot 集成 tio 网络编程框架 待完成 |
-| [spring-boot-demo-grpc](./spring-boot-demo-grpc) | spring-boot 集成grpc,配置tls/ssl,参见[ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5) 待完成 |
-| [spring-boot-demo-codegen](./spring-boot-demo-codegen) | spring-boot 集成 velocity 模板技术实现的代码生成器,简化开发 |
-| [spring-boot-demo-graylog](./spring-boot-demo-graylog) | spring-boot 集成 graylog 实现日志统一收集 |
-| spring-boot-demo-sso | spring-boot 集成 SSO 单点登录,参见 [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12) 待完成 |
-| [spring-boot-demo-ldap](./spring-boot-demo-ldap) | spring-boot 集成 LDAP,集成 `spring-boot-starter-data-ldap` 完成对 Ldap 的基本 CURD操作, 并给出以登录为实战的 API 示例,参见 [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23),感谢 [@fxbin](https://github.com/fxbin) |
-| [spring-boot-demo-dynamic-datasource](./spring-boot-demo-dynamic-datasource) | spring-boot 动态添加数据源、动态切换数据源 |
-| [spring-boot-demo-ratelimit-guava](./spring-boot-demo-ratelimit-guava) | spring-boot 使用 Guava RateLimiter 实现单机版限流,保护 API |
-| [spring-boot-demo-ratelimit-redis](./spring-boot-demo-ratelimit-redis) | spring-boot 使用 Redis + Lua 脚本实现分布式限流,保护 API |
-| [spring-boot-demo-https](./spring-boot-demo-https) | spring-boot 集成 HTTPS |
-| [spring-boot-demo-elasticsearch-rest-high-level-client](./spring-boot-demo-elasticsearch-rest-high-level-client) | spring boot 集成 ElasticSearch 7.x 版本,使用官方 Rest High Level Client 操作 ES 数据 |
-| [spring-boot-demo-flyway](./spring-boot-demo-flyway) | spring boot 集成 Flyway,项目启动时初始化数据库表结构,同时支持数据库脚本版本控制 |
-
-## License
-
-[MIT](http://opensource.org/licenses/MIT)
-
-Copyright (c) 2018 Yangkai.Shen
-
## 项目趋势
[![Stargazers over time](https://starchart.cc/xkcoding/spring-boot-demo.svg)](https://starchart.cc/xkcoding/spring-boot-demo)
-## 附录
+## 其他
-### 根目录下的 pom.xml
+### 团队纳新
-```xml
-
+组内招人啦,HC 巨多,Base 杭州,感兴趣的小伙伴,查看 [岗位详情](./jd.md)
-
- 4.0.0
+### 开源推荐
- com.xkcoding
- spring-boot-demo
- 1.0.0-SNAPSHOT
-
- spring-boot-demo-helloworld
- spring-boot-demo-properties
- spring-boot-demo-actuator
- spring-boot-demo-admin
- spring-boot-demo-logback
- spring-boot-demo-log-aop
- spring-boot-demo-exception-handler
- spring-boot-demo-template-freemarker
- spring-boot-demo-template-thymeleaf
- spring-boot-demo-template-beetl
- spring-boot-demo-template-enjoy
- spring-boot-demo-orm-jdbctemplate
- spring-boot-demo-orm-jpa
- spring-boot-demo-orm-mybatis
- spring-boot-demo-orm-mybatis-mapper-page
- spring-boot-demo-orm-mybatis-plus
- spring-boot-demo-orm-beetlsql
- spring-boot-demo-upload
- spring-boot-demo-cache-redis
- spring-boot-demo-cache-ehcache
- spring-boot-demo-email
- spring-boot-demo-task
- spring-boot-demo-task-quartz
- spring-boot-demo-task-xxl-job
- spring-boot-demo-swagger
- spring-boot-demo-swagger-beauty
- spring-boot-demo-rbac-security
- spring-boot-demo-rbac-shiro
- spring-boot-demo-session
- spring-boot-demo-oauth
- spring-boot-demo-social
- spring-boot-demo-zookeeper
- spring-boot-demo-mq-rabbitmq
- spring-boot-demo-mq-rocketmq
- spring-boot-demo-mq-kafka
- spring-boot-demo-websocket
- spring-boot-demo-websocket-socketio
- spring-boot-demo-ureport2
- spring-boot-demo-uflo
- spring-boot-demo-urule
- spring-boot-demo-activiti
- spring-boot-demo-async
- spring-boot-demo-dubbo
- spring-boot-demo-war
- spring-boot-demo-elasticsearch
- spring-boot-demo-mongodb
- spring-boot-demo-neo4j
- spring-boot-demo-docker
- spring-boot-demo-multi-datasource-jpa
- spring-boot-demo-multi-datasource-mybatis
- spring-boot-demo-sharding-jdbc
- spring-boot-demo-tio
- spring-boot-demo-codegen
- spring-boot-demo-graylog
- spring-boot-demo-ldap
- spring-boot-demo-dynamic-datasource
- spring-boot-demo-ratelimit-guava
- spring-boot-demo-ratelimit-redis
- spring-boot-demo-elasticsearch-rest-high-level-client
- spring-boot-demo-https
- spring-boot-demo-flyway
-
- pom
+- `JustAuth`:史上最全的整合第三方登录的开源库,https://github.com/justauth/JustAuth
+- `Mica`:SpringBoot 微服务高效开发工具集,https://github.com/lets-mica/mica
+- `awesome-collector`:https://github.com/P-P-X/awesome-collector
+- `SpringBlade`:完整的线上解决方案(企业开发必备),https://github.com/chillzhuang/SpringBlade
+- `Pig`:宇宙最强微服务认证授权脚手架(架构师必备),https://github.com/pigxcloud/pig
- spring-boot-demo
- http://xkcoding.com
+### 开发计划
-
- UTF-8
- UTF-8
- 1.8
- 1.8
- 1.8
- 2.1.0.RELEASE
- 8.0.12
- 5.0.0
- 28.1-jre
- 1.20
-
+查看 [TODO](./TODO.md) 文件
-
-
- aliyun
- aliyun
- https://maven.aliyun.com/repository/public
-
- true
-
-
- false
-
-
-
+### 各 Module 介绍
-
-
-
- org.springframework.boot
- spring-boot-dependencies
- ${spring.boot.version}
- pom
- import
-
-
- mysql
- mysql-connector-java
- ${mysql.version}
-
-
-
- cn.hutool
- hutool-all
- ${hutool.version}
-
-
-
- com.google.guava
- guava
- ${guava.version}
-
-
-
- eu.bitwalker
- UserAgentUtils
- ${user.agent.version}
-
-
-
+| Module 名称 | Module 介绍 |
+| ------------------------------------------------------------ | ------------------------------------------------------------ |
+| [demo-helloworld](./demo-helloworld) | spring-boot 的一个 helloworld |
+| [demo-properties](./demo-properties) | spring-boot 读取配置文件中的内容 |
+| [demo-actuator](./demo-actuator) | spring-boot 集成 spring-boot-starter-actuator 用于监控 spring-boot 的启动和运行状态 |
+| [demo-admin-client](./demo-admin/admin-client) | spring-boot 集成 spring-boot-admin 来可视化的监控 spring-boot 程序的运行状态,可以与 actuator 互相搭配使用,客户端示例 |
+| [demo-admin-server](./demo-admin/admin-server) | spring-boot 集成 spring-boot-admin 来可视化的监控 spring-boot 程序的运行状态,可以与 actuator 互相搭配使用,服务端示例 |
+| [demo-logback](./demo-logback) | spring-boot 集成 logback 日志 |
+| [demo-log-aop](./demo-log-aop) | spring-boot 使用 AOP 切面的方式记录 web 请求日志 |
+| [demo-exception-handler](./demo-exception-handler) | spring-boot 统一异常处理,包括2种,第一种返回统一的 json 格式,第二种统一跳转到异常页面 |
+| [demo-template-freemarker](./demo-template-freemarker) | spring-boot 集成 Freemarker 模板引擎 |
+| [demo-template-thymeleaf](./demo-template-thymeleaf) | spring-boot 集成 Thymeleaf 模板引擎 |
+| [demo-template-beetl](./demo-template-beetl) | spring-boot 集成 Beetl 模板引擎 |
+| [demo-template-enjoy](./demo-template-enjoy) | spring-boot 集成 Enjoy 模板引擎 |
+| [demo-orm-jdbctemplate](./demo-orm-jdbctemplate) | spring-boot 集成 Jdbc Template 操作数据库,并简易封装通用 Dao 层 |
+| [demo-orm-jpa](./demo-orm-jpa) | spring-boot 集成 spring-boot-starter-data-jpa 操作数据库 |
+| [demo-orm-mybatis](./demo-orm-mybatis) | spring-boot 集成原生mybatis,使用 [mybatis-spring-boot-starter](https://github.com/mybatis/spring-boot-starter) 集成 |
+| [demo-orm-mybatis-mapper-page](./demo-orm-mybatis-mapper-page) | spring-boot 集成[通用Mapper](https://github.com/abel533/Mapper)和[PageHelper](https://github.com/pagehelper/Mybatis-PageHelper),使用 [mapper-spring-boot-starter](https://github.com/abel533/Mapper/tree/master/spring-boot-starter) 和 [pagehelper-spring-boot-starter](https://github.com/pagehelper/pagehelper-spring-boot) 集成 |
+| [demo-orm-mybatis-plus](./demo-orm-mybatis-plus) | spring-boot 集成 [mybatis-plus](https://mybatis.plus/),使用 [mybatis-plus-boot-starter](http://mp.baomidou.com/) 集成,集成 BaseMapper、BaseService、ActiveRecord 操作数据库 |
+| [demo-orm-beetlsql](./demo-orm-beetlsql) | spring-boot 集成 [beetl-sql](http://ibeetl.com/guide/#beetlsql),使用 [beetl-framework-starter](http://ibeetl.com/guide/#beetlsql) 集成 |
+| [demo-upload](./demo-upload) | spring-boot 文件上传示例,包含本地文件上传以及七牛云文件上传 |
+| [demo-cache-redis](./demo-cache-redis) | spring-boot 整合 redis,操作redis中的数据,并使用redis缓存数据 |
+| [demo-cache-ehcache](./demo-cache-ehcache) | spring-boot 整合 ehcache,使用 ehcache 缓存数据 |
+| [demo-email](./demo-email) | spring-boot 整合 email,包括发送简单文本邮件、HTML邮件(包括模板HTML邮件)、附件邮件、静态资源邮件 |
+| [demo-task](./demo-task) | spring-boot 快速实现定时任务 |
+| [demo-task-quartz](./demo-task-quartz) | spring-boot 整合 quartz,并实现对定时任务的管理,包括新增定时任务,删除定时任务,暂停定时任务,恢复定时任务,修改定时任务启动时间,以及定时任务列表查询,`提供前端页面` |
+| [demo-task-xxl-job](./demo-task-xxl-job) | spring-boot 整合[xxl-job](http://www.xuxueli.com/xxl-job/en/#/),并提供绕过 `xxl-job-admin` 对定时任务的管理的方法,包括定时任务列表,触发器列表,新增定时任务,删除定时任务,停止定时任务,启动定时任务,修改定时任务,手动触发定时任务 |
+| [demo-swagger](./demo-swagger) | spring-boot 集成原生的 `swagger` 用于统一管理、测试 API 接口 |
+| [demo-swagger-beauty](./demo-swagger-beauty) | spring-boot 集成第三方 `swagger` [swagger-bootstrap-ui](https://github.com/xiaoymin/Swagger-Bootstrap-UI) 美化API文档样式,用于统一管理、测试 API 接口 |
+| [demo-rbac-security](./demo-rbac-security) | spring-boot 集成 spring security 完成基于RBAC权限模型的权限管理,支持自定义过滤请求,动态权限认证,使用 JWT 安全认证,支持在线人数统计,手动踢出用户等操作 |
+| [demo-rbac-shiro](./demo-rbac-shiro) | spring-boot 集成 shiro 实现权限管理 待完成 |
+| [demo-session](./demo-session) | spring-boot 集成 Spring Session 实现Session共享、重启程序Session不失效 |
+| [demo-oauth](./demo-oauth) | spring-boot 实现 oauth 服务器功能,实现授权码机制 待完成 |
+| [demo-social](./demo-social) | spring-boot 集成第三方登录,集成 `justauth-spring-boot-starter` 实现QQ登录、GitHub登录、微信登录、谷歌登录、微软登录、小米登录、企业微信登录。 |
+| [demo-zookeeper](./demo-zookeeper) | spring-boot 集成 Zookeeper 结合AOP实现分布式锁 |
+| [demo-mq-rabbitmq](./demo-mq-rabbitmq) | spring-boot 集成 RabbitMQ 实现基于直接队列模式、分列模式、主题模式、延迟队列的消息发送和接收 |
+| [demo-mq-rocketmq](./demo-mq-rocketmq) | spring-boot 集成 RocketMQ,实现消息的发送和接收 待完成 |
+| [demo-mq-kafka](./demo-mq-kafka) | spring-boot 集成 kafka,实现消息的发送和接收 |
+| [demo-websocket](./demo-websocket) | spring-boot 集成 websocket,后端主动推送前端服务器运行信息 |
+| [demo-websocket-socketio](./demo-websocket-socketio) | spring-boot 使用 netty-socketio 集成 websocket,实现一个简单的聊天室 |
+| [demo-ureport2](./demo-ureport2) | spring-boot 集成 ureport2 实现复杂的自定义的中国式报表 待完成 |
+| [demo-uflo](./demo-uflo) | spring-boot 集成 uflo 快速实现轻量级流程引擎 待完成 |
+| [demo-urule](./demo-urule) | spring-boot 集成 urule 快速实现规则引擎 待完成 |
+| [demo-activiti](./demo-activiti) | spring-boot 集成 activiti 7 流程引擎 待完成 |
+| [demo-async](./demo-async) | spring-boot 使用原生提供的异步任务支持,实现异步执行任务 |
+| [demo-war](./demo-war) | spring-boot 打成 war 包的配置 |
+| [demo-elasticsearch](./demo-elasticsearch) | spring-boot 集成 ElasticSearch,集成 `spring-boot-starter-data-elasticsearch` 完成对 ElasticSearch 的高级使用技巧,包括创建索引、配置映射、删除索引、增删改查基本操作、复杂查询、高级查询、聚合查询等 |
+| [demo-dubbo](./demo-dubbo) | spring-boot 集成 Dubbo,分别为公共模块 `spring-boot-demo-dubbo-common`、服务提供方`spring-boot-demo-dubbo-provider`、服务调用方`spring-boot-demo-dubbo-consumer` |
+| [demo-mongodb](./demo-mongodb) | spring-boot 集成 MongoDB,使用官方的 starter 实现增删改查 |
+| [demo-neo4j](./demo-neo4j) | spring-boot 集成 Neo4j 图数据库,实现一个校园人物关系网的demo |
+| [demo-docker](./demo-docker) | spring-boot 容器化 |
+| [demo-multi-datasource-jpa](./demo-multi-datasource-jpa) | spring-boot 使用JPA集成多数据源 |
+| [demo-multi-datasource-mybatis](./demo-multi-datasource-mybatis) | spring-boot 使用Mybatis集成多数据源,使用 Mybatis-Plus 提供的开源解决方案实现 |
+| [demo-sharding-jdbc](./demo-sharding-jdbc) | spring-boot 使用 `sharding-jdbc` 实现分库分表,同时ORM采用 Mybatis-Plus |
+| [demo-tio](./demo-tio) | spring-boot 集成 tio 网络编程框架 待完成 |
+| demo-grpc | spring-boot 集成grpc,配置tls/ssl,参见[ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5) 待完成 |
+| [demo-codegen](./demo-codegen) | spring-boot 集成 velocity 模板技术实现的代码生成器,简化开发 |
+| [demo-graylog](./demo-graylog) | spring-boot 集成 graylog 实现日志统一收集 |
+| demo-sso | spring-boot 集成 SSO 单点登录,参见 [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12) 待完成 |
+| [demo-ldap](./demo-ldap) | spring-boot 集成 LDAP,集成 `spring-boot-starter-data-ldap` 完成对 Ldap 的基本 CURD操作, 并给出以登录为实战的 API 示例,参见 [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23),感谢 [@fxbin](https://github.com/fxbin) |
+| [demo-dynamic-datasource](./demo-dynamic-datasource) | spring-boot 动态添加数据源、动态切换数据源 |
+| [demo-ratelimit-guava](./demo-ratelimit-guava) | spring-boot 使用 Guava RateLimiter 实现单机版限流,保护 API |
+| [demo-ratelimit-redis](./demo-ratelimit-redis) | spring-boot 使用 Redis + Lua 脚本实现分布式限流,保护 API |
+| [demo-https](./demo-https) | spring-boot 集成 HTTPS |
+| [demo-elasticsearch-rest-high-level-client](./demo-elasticsearch-rest-high-level-client) | spring boot 集成 ElasticSearch 7.x 版本,使用官方 Rest High Level Client 操作 ES 数据 |
+| [demo-flyway](./demo-flyway) | spring boot 集成 Flyway,项目启动时初始化数据库表结构,同时支持数据库脚本版本控制 |
+| [demo-ureport2](./demo-ureport2) | spring boot 集成 Ureport2,实现中国式复杂报表设计 |
+
+### 感谢
-
-
-
-
- maven-clean-plugin
- 3.0.0
-
-
- maven-resources-plugin
- 3.0.2
-
-
- maven-compiler-plugin
- 3.7.0
-
-
- maven-surefire-plugin
- 2.20.1
-
-
- maven-jar-plugin
- 3.0.2
-
-
- maven-install-plugin
- 2.5.2
-
-
- maven-deploy-plugin
- 2.8.2
-
-
- org.springframework.boot
- spring-boot-maven-plugin
- ${spring.boot.version}
-
-
-
- repackage
-
-
-
-
-
-
-
-
-```
+- **感谢 JetBrains 提供的免费开源 License**
-### 官方提供的 starter 介绍
+- [感谢史上最牛的代码生成插件 MyBatisCodeHelper-Pro 提供的永久激活码](https://gejun123456.github.io/MyBatisCodeHelper-Pro/#/?id=mybatiscodehelper-pro)
-| 名称 | 描述 |
-| :------------------------------------- | :---------------------------------------------------------- |
-| spring-boot-starter | Spring Boot 核心包,包括自动装配,日志,以及YAML文件解析 |
-| spring-boot-starter-actuator | 帮助在生产环境下监控和管理 Spring Boot 应用 |
-| spring-boot-starter-amqp | Spring Boot 快速集成 RabbitMQ |
-| spring-boot-starter-aop | 提供切面编程特性,包含 spring-aop 和 AspectJ 依赖 |
-| spring-boot-starter-batch | 快速集成 Spring Batch 批处理框架,包括操作 HSQLDB 数据库 |
-| spring-boot-starter-cache | Support for Spring’s Cache abstraction. |
-| spring-boot-starter-data-elasticsearch | Spring Boot 快速集成 ElasticSearch 查询分析引擎 |
-| spring-boot-starter-data-jpa | Spring Boot 快速集成 JPA 操作数据库 |
-| spring-boot-starter-data-mongodb | Spring Boot 快速集成 MongoDB 非关系型数据库 |
-| spring-boot-starter-data-rest | Spring Boot 暴露数据库查询端点为 REST 服务 |
-| spring-boot-starter-data-solr | Spring Boot 快速集成 Solr 实现全文索引 |
-| spring-boot-starter-freemarker | 提供 FreeMarker 模板引擎 |
-| spring-boot-starter-groovy-templates | 提供 Groovy 模板引擎 |
-| spring-boot-starter-integration | 提供通用的集成 spring-integration 模块 |
-| spring-boot-starter-jdbc | 快速集成 JDBC 操作数据库 |
-| spring-boot-starter-jersey | 提供 Jersey 提供 RESTful 服务 |
-| spring-boot-starter-jta-atomikos | 集成 JTA Atomikos 实现分布式事务 |
-| spring-boot-starter-jta-bitronix | 集成 JTA Bitronix 实现分布式事务 |
-| spring-boot-starter-mail | 快速邮件集成 |
-| spring-boot-starter-mustache | 提供 Mustache 模板引擎 |
-| spring-boot-starter-redis | Spring Boot 快速集成 Redis |
-| spring-boot-starter-security | Support for spring-security. |
-| spring-boot-starter-social-facebook | Support for spring-social-facebook. |
-| spring-boot-starter-social-linkedin | Support for spring-social-linkedin. |
-| spring-boot-starter-social-twitter | Support for spring-social-twitter. |
-| spring-boot-starter-test | 提供通用单元测试依赖,包括 JUnit, Hamcrest , Mockito |
-| spring-boot-starter-thymeleaf | 提供 Thymeleaf 模板引擎,包括 Thymeleaf 的自动装配等 |
-| spring-boot-starter-velocity | 提供 Velocity 模板引擎 |
-| spring-boot-starter-web | 提供全栈的 web 开发特性,包括 Spring MVC 依赖和 Tomcat 容器 |
-| spring-boot-starter-websocket | Spring Boot 集成 WebSocket 功能 |
-| spring-boot-starter-ws | Spring Boot 集成 WebService 功能 |
+### License
-### 开源推荐
+[MIT](http://opensource.org/licenses/MIT)
-- `JustAuth`:史上最全的整合第三方登录的开源库,https://github.com/justauth/JustAuth
-- `Mica`:SpringBoot 微服务高效开发工具集,https://github.com/lets-mica/mica
-- `awesome-collector`:https://github.com/P-P-X/awesome-collector
-- `SpringBlade`:完整的线上解决方案(企业开发必备),https://github.com/chillzhuang/SpringBlade
-- `Pig`:宇宙最强微服务认证授权脚手架(架构师必备),https://github.com/pigxcloud/pig
+Copyright (c) 2018 Yangkai.Shen
\ No newline at end of file
diff --git a/TODO.en.md b/TODO.en.md
index bc93416..ae1bca2 100644
--- a/TODO.en.md
+++ b/TODO.en.md
@@ -1,73 +1,73 @@
# spring-boot-demo Project TODO List
-## Module plan (completed: 54 / 66)
+## Module plan (completed: 55 / 66)
-- [x] ~~spring-boot-demo-helloworld(helloworld example)~~
-- [x] ~~spring-boot-demo-properties (read configuration file information)~~
-- [x] ~~spring-boot-demo-actuator (endpoint monitoring for Spring boot)~~
-- [x] ~~spring-boot-demo-admin-client (for Spring boot visual control client)~~
-- [x] ~~spring-boot-demo-admin-server (for Spring boot visual control server)~~
-- [x] ~~spring-boot-demo-logback (integrated logback log)~~
-- [x] ~~spring-boot-demo-log-aop (use AOP to intercept request log information)~~
-- [x] ~~spring-boot-demo-exception-handler (unified exception handling)~~
-- [x] ~~spring-boot-demo-template-freemarker (using template engine - Freemarker)~~
-- [x] ~~spring-boot-demo-template-thymeleaf (using template engine - thymeleaf)~~
-- [x] ~~spring-boot-demo-template-beetl (using template engine - beetl)~~
-- [x] ~~spring-boot-demo-template-enjoy (using template engine - JFinal-Enjoy)~~
-- [x] ~~spring-boot-demo-upload (upload - integrated local upload and seven cattle cloud upload)~~
-- [x] ~~spring-boot-demo-orm-jdbctemplate (operating SQL relational database - JdbcTemplate)~~
-- [x] ~~spring-boot-demo-orm-jpa (operating SQL Relational Database - JPA)~~
-- [x] ~~spring-boot-demo-orm-mybatis (operating SQL relational database - mybatis)~~
-- [x] ~~spring-boot-demo-orm-mybatis-mapper-page (operating SQL relational database - integrating mybatis generic Mapper, PageHelper)~~
-- [x] ~~spring-boot-demo-orm-mybatis-plus (operating SQL relational database - integrating mybatis-plus, Mapper, ActiveRecord)~~
-- [x] ~~spring-boot-demo-orm-beetlsql (operating SQL relational database - beetlSQL)~~
-- [x] ~~spring-boot-demo-cache-redis (using redis for caching)~~
-- [x] ~~spring-boot-demo-cache-ehcache (using Ehcache for caching)~~
-- [x] ~~spring-boot-demo-email (integrated mail service)~~
-- [x] ~~spring-boot-demo-task (scheduled task - Task implementation)~~
-- [x] ~~spring-boot-demo-task-quartz (scheduled task - Quartz implementation)~~
-- [x] ~~spring-boot-demo-task-xxl-job (scheduled task - XXL-JOB for Distributed Scheduling)~~
-- [x] ~~spring-boot-demo-swagger (integrated Swagger for API interface test management)~~
-- [x] ~~spring-boot-demo-swagger-beauty (integrated custom and more beautiful Swagger test management of API interface)~~
-- [x] ~~spring-boot-demo-rbac-security (implementing RBAC-based permission model - Spring Security)~~
-- [ ] spring-boot-demo-rbac-shiro (implementing RBAC-based permission model - shiro)
-- [x] ~~spring-boot-demo-session(unified Session Management)~~
-- [ ] spring-boot-demo-oauth (OAuth2 certification)
-- [x] ~~spring-boot-demo-social (integrated JustAuth implements third-party authorization verification, and implements third-party logins such as QQ, WeChat, GitHub, Google, Xiaomi, etc.)~~
-- [x] ~~spring-boot-demo-zookeeper (use zookeeper to implement distributed locks with AOP)~~
-- [x] ~~spring-boot-demo-mq-rabbitmq (integrated messaging middleware - RabbitMQ)~~
-- [ ] spring-boot-demo-mq-rocketmq (integrated messaging middleware - RocketMQ)
-- [x] ~~spring-boot-demo-mq-kafka (integrated message middleware - Kafka)~~
-- [x] ~~spring-boot-demo-websocket (integrated websocket service)~~
-- [x] ~~spring-boot-demo-websocket-socketio (integrated socketio implements websocket service)~~
-- [ ] spring-boot-demo-ureport2 (integrated ureport2 implements a custom complex Chinese-style reporting engine)
-- [ ] spring-boot-demo-uflo (integrated uflo implementation process control engine)
-- [ ] spring-boot-demo-urule (integrated urule implementation rules engine)
-- [ ] spring-boot-demo-activiti (integrated of Activiti to implement process control engine)
-- [x] ~~spring-boot-demo-async (Spring boot implements asynchronous calls)~~
-- [x] ~~spring-boot-demo-dubbo (integrated dubbo)~~
-- [x] ~~spring-boot-demo-war (packaged into a war package)~~
-- [x] ~~spring-boot-demo-elasticsearch (integrated ElasticSearch)~~
-- [x] ~~spring-boot-demo-mongodb (integrated MongoDb)~~
-- [x] ~~spring-boot-demo-neo4j (integrated neo4j graph database)~~
-- [x] ~~spring-boot-demo-docker (packaged into docker image)~~
-- [x] ~~spring-boot-demo-multi-datasource-jpa (integrated JPA multi data source)~~
-- [x] ~~spring-boot-demo-multi-datasource-mybatis (integrated with mybatis multi-data source)~~
-- [x] ~~spring-boot-demo-sharding-jdbc (integrated sharding-jdbc implementation sub-library table)~~
-- [ ] spring-boot-demo-tio (integrated t-io)
-- [ ] spring-boot-demo-grpc (integrated grpc, configure tls/ssl) see [ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5)
-- [x] ~~spring-boot-demo-codegen (integrated velocity auto-generated code)~~
-- [x] ~~spring-boot-demo-graylog (integrated gralog log management)~~
-- [ ] spring-boot-demo-sso (integrated single sign on) see [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12)
-- [x] ~~spring-boot-demo-ldap (integrated ldap)see [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23)~~
-- [x] ~~spring-boot-demo-dynamic-datasource(add datasource dynamically, switch datasource dynamically)~~
-- [x] ~~spring-boot-demo-ratelimit-guava(use Guava RateLimiter to protect API by standalone rate limiting)~~
-- [x] ~~spring-boot-demo-ratelimit-redis(use Redis and Lua script implementation to protect API by distributed rate limiting)~~
-- [x] ~~spring-boot-demo-https(integrated HTTPS)~~
-- [x] ~~spring-boot-demo-elasticsearch-rest-high-level-client(integrated Elasticsearch 7.x version,use official Rest High Level Client to operate ES data)~~
-- [ ] spring-boot-demo-springbatch(data process)
-- [ ] spring-boot-demo-security-justauth(use JustAuth to login GitHub,and use Spring-Security to manage login state)
-- [x] ~~spring-boot-demo-flyway(integrated Flyway to initialize tables and data in database, Flyway also support the sql script version control)~~
+- [x] ~~demo-helloworld(helloworld example)~~
+- [x] ~~demo-properties (read configuration file information)~~
+- [x] ~~demo-actuator (endpoint monitoring for Spring boot)~~
+- [x] ~~demo-admin-client (for Spring boot visual control client)~~
+- [x] ~~demo-admin-server (for Spring boot visual control server)~~
+- [x] ~~demo-logback (integrated logback log)~~
+- [x] ~~demo-log-aop (use AOP to intercept request log information)~~
+- [x] ~~demo-exception-handler (unified exception handling)~~
+- [x] ~~demo-template-freemarker (using template engine - Freemarker)~~
+- [x] ~~demo-template-thymeleaf (using template engine - thymeleaf)~~
+- [x] ~~demo-template-beetl (using template engine - beetl)~~
+- [x] ~~demo-template-enjoy (using template engine - JFinal-Enjoy)~~
+- [x] ~~demo-upload (upload - integrated local upload and seven cattle cloud upload)~~
+- [x] ~~demo-orm-jdbctemplate (operating SQL relational database - JdbcTemplate)~~
+- [x] ~~demo-orm-jpa (operating SQL Relational Database - JPA)~~
+- [x] ~~demo-orm-mybatis (operating SQL relational database - mybatis)~~
+- [x] ~~demo-orm-mybatis-mapper-page (operating SQL relational database - integrating mybatis generic Mapper, PageHelper)~~
+- [x] ~~demo-orm-mybatis-plus (operating SQL relational database - integrating mybatis-plus, Mapper, ActiveRecord)~~
+- [x] ~~demo-orm-beetlsql (operating SQL relational database - beetlSQL)~~
+- [x] ~~demo-cache-redis (using redis for caching)~~
+- [x] ~~demo-cache-ehcache (using Ehcache for caching)~~
+- [x] ~~demo-email (integrated mail service)~~
+- [x] ~~demo-task (scheduled task - Task implementation)~~
+- [x] ~~demo-task-quartz (scheduled task - Quartz implementation)~~
+- [x] ~~demo-task-xxl-job (scheduled task - XXL-JOB for Distributed Scheduling)~~
+- [x] ~~demo-swagger (integrated Swagger for API interface test management)~~
+- [x] ~~demo-swagger-beauty (integrated custom and more beautiful Swagger test management of API interface)~~
+- [x] ~~demo-rbac-security (implementing RBAC-based permission model - Spring Security)~~
+- [ ] demo-rbac-shiro (implementing RBAC-based permission model - shiro)
+- [x] ~~demo-session(unified Session Management)~~
+- [ ] demo-oauth (OAuth2 certification)
+- [x] ~~demo-social (integrated JustAuth implements third-party authorization verification, and implements third-party logins such as QQ, WeChat, GitHub, Google, Xiaomi, etc.)~~
+- [x] ~~demo-zookeeper (use zookeeper to implement distributed locks with AOP)~~
+- [x] ~~demo-mq-rabbitmq (integrated messaging middleware - RabbitMQ)~~
+- [ ] demo-mq-rocketmq (integrated messaging middleware - RocketMQ)
+- [x] ~~demo-mq-kafka (integrated message middleware - Kafka)~~
+- [x] ~~demo-websocket (integrated websocket service)~~
+- [x] ~~demo-websocket-socketio (integrated socketio implements websocket service)~~
+- [x] ~~demo-ureport2 (integrated ureport2 implements a custom complex Chinese-style reporting engine)~~
+- [ ] demo-uflo (integrated uflo implementation process control engine)
+- [ ] demo-urule (integrated urule implementation rules engine)
+- [ ] demo-activiti (integrated of Activiti to implement process control engine)
+- [x] ~~demo-async (Spring boot implements asynchronous calls)~~
+- [x] ~~demo-dubbo (integrated dubbo)~~
+- [x] ~~demo-war (packaged into a war package)~~
+- [x] ~~demo-elasticsearch (integrated ElasticSearch)~~
+- [x] ~~demo-mongodb (integrated MongoDb)~~
+- [x] ~~demo-neo4j (integrated neo4j graph database)~~
+- [x] ~~demo-docker (packaged into docker image)~~
+- [x] ~~demo-multi-datasource-jpa (integrated JPA multi data source)~~
+- [x] ~~demo-multi-datasource-mybatis (integrated with mybatis multi-data source)~~
+- [x] ~~demo-sharding-jdbc (integrated sharding-jdbc implementation sub-library table)~~
+- [ ] demo-tio (integrated t-io)
+- [ ] demo-grpc (integrated grpc, configure tls/ssl) see [ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5)
+- [x] ~~demo-codegen (integrated velocity auto-generated code)~~
+- [x] ~~demo-graylog (integrated gralog log management)~~
+- [ ] demo-sso (integrated single sign on) see [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12)
+- [x] ~~demo-ldap (integrated ldap)see [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23)~~
+- [x] ~~demo-dynamic-datasource(add datasource dynamically, switch datasource dynamically)~~
+- [x] ~~demo-ratelimit-guava(use Guava RateLimiter to protect API by standalone rate limiting)~~
+- [x] ~~demo-ratelimit-redis(use Redis and Lua script implementation to protect API by distributed rate limiting)~~
+- [x] ~~demo-https(integrated HTTPS)~~
+- [x] ~~demo-elasticsearch-rest-high-level-client(integrated Elasticsearch 7.x version,use official Rest High Level Client to operate ES data)~~
+- [ ] demo-springbatch(data process)
+- [ ] demo-security-justauth(use JustAuth to login GitHub,and use Spring-Security to manage login state)
+- [x] ~~demo-flyway(integrated Flyway to initialize tables and data in database, Flyway also support the sql script version control)~~
## Remarks
diff --git a/TODO.md b/TODO.md
index 9f90735..0b35d26 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,73 +1,73 @@
# spring-boot-demo 项目待办列表
-## 模块计划(已完成:54 / 66)
+## 模块计划(已完成:55 / 66)
-- [x] ~~spring-boot-demo-helloworld(Helloworld 示例)~~
-- [x] ~~spring-boot-demo-properties(读取配置文件信息)~~
-- [x] ~~spring-boot-demo-actuator(对 Spring boot 的端点监控)~~
-- [x] ~~spring-boot-demo-admin-client(对 Spring boot 可视化管控 客户端)~~
-- [x] ~~spring-boot-demo-admin-server(对 Spring boot 可视化管控 服务端)~~
-- [x] ~~spring-boot-demo-logback(集成 logback 日志)~~
-- [x] ~~spring-boot-demo-log-aop(使用 AOP 拦截请求日志信息)~~
-- [x] ~~spring-boot-demo-exception-handler(统一异常处理)~~
-- [x] ~~spring-boot-demo-template-freemarker(使用模板引擎 - Freemarker)~~
-- [x] ~~spring-boot-demo-template-thymeleaf(使用模板引擎 - thymeleaf)~~
-- [x] ~~spring-boot-demo-template-beetl(使用模板引擎 - beetl)~~
-- [x] ~~spring-boot-demo-template-enjoy(使用模板引擎 - JFinal-Enjoy)~~
-- [x] ~~spring-boot-demo-upload(上传 - 集成本地上传和七牛云上传)~~
-- [x] ~~spring-boot-demo-orm-jdbctemplate(操作 SQL 关系型数据库 - JdbcTemplate)~~
-- [x] ~~spring-boot-demo-orm-jpa(操作 SQL 关系型数据库 - JPA)~~
-- [x] ~~spring-boot-demo-orm-mybatis(操作 SQL 关系型数据库 - mybatis)~~
-- [x] ~~spring-boot-demo-orm-mybatis-mapper-page(操作 SQL 关系型数据库 - 集成mybatis通用Mapper,PageHelper)~~
-- [x] ~~spring-boot-demo-orm-mybatis-plus(操作 SQL 关系型数据库 - 集成mybatis-plus,Mapper操作、ActiveRecord操作)~~
-- [x] ~~spring-boot-demo-orm-beetlsql(操作 SQL 关系型数据库 - beetlSQL)~~
-- [x] ~~spring-boot-demo-cache-redis(使用 redis 进行缓存)~~
-- [x] ~~spring-boot-demo-cache-ehcache(使用 Ehcache 进行缓存)~~
-- [x] ~~spring-boot-demo-email(集成邮件服务)~~
-- [x] ~~spring-boot-demo-task(定时任务 - Task 实现)~~
-- [x] ~~spring-boot-demo-task-quartz(定时任务 - Quartz 实现)~~
-- [x] ~~spring-boot-demo-task-xxl-job(定时任务 - XXL-JOB 实现分布式调度)~~
-- [x] ~~spring-boot-demo-swagger(集成 Swagger 对 API 接口进行测试管理)~~
-- [x] ~~spring-boot-demo-swagger-beauty(集成自定义且更加美观的 Swagger 对 API 接口进行测试管理)~~
-- [x] ~~spring-boot-demo-rbac-security(实现基于 RBAC 的权限模型 - Spring Security)~~
-- [ ] spring-boot-demo-rbac-shiro(实现基于 RBAC 的权限模型 - shiro)
-- [x] ~~spring-boot-demo-session(统一 Session 管理)~~
-- [ ] spring-boot-demo-oauth(OAuth2 认证)
-- [x] ~~spring-boot-demo-social(集成 JustAuth 实现第三方授权验证,实现 QQ、微信、GitHub、谷歌、小米等第三方登录)~~
-- [x] ~~spring-boot-demo-zookeeper(使用 zookeeper 结合AOP实现分布式锁)~~
-- [x] ~~spring-boot-demo-mq-rabbitmq(集成消息中间件 - RabbitMQ)~~
-- [ ] spring-boot-demo-mq-rocketmq(集成消息中间件 - RocketMQ)
-- [x] ~~spring-boot-demo-mq-kafka(集成消息中间件 - Kafka)~~
-- [x] ~~spring-boot-demo-websocket(集成 websocket 服务)~~
-- [x] ~~spring-boot-demo-websocket-socketio(集成 socketio 实现 websocket 服务)~~
-- [ ] spring-boot-demo-ureport2 (集成 ureport2 实现自定义的复杂中国式报表引擎)
-- [ ] spring-boot-demo-uflo(集成 uflo 实现流程控制引擎)
-- [ ] spring-boot-demo-urule(集成 urule 实现规则引擎)
-- [ ] spring-boot-demo-activiti(集成 Activiti 实现流程控制引擎)
-- [x] ~~spring-boot-demo-async(Spring boot 实现异步调用)~~
-- [x] ~~spring-boot-demo-dubbo(集成 dubbo)~~
-- [x] ~~spring-boot-demo-war(打包成war包)~~
-- [x] ~~spring-boot-demo-elasticsearch(集成 ElasticSearch)~~
-- [x] ~~spring-boot-demo-mongodb(集成 MongoDb)~~
-- [x] ~~spring-boot-demo-neo4j(集成 neo4j 图数据库)~~
-- [x] ~~spring-boot-demo-docker(打包成 docker 镜像)~~
-- [x] ~~spring-boot-demo-multi-datasource-jpa(集成JPA多数据源)~~
-- [x] ~~spring-boot-demo-multi-datasource-mybatis(集成mybatis多数据源)~~
-- [x] ~~spring-boot-demo-sharding-jdbc(集成 sharding-jdbc 实现分库分表)~~
-- [ ] spring-boot-demo-tio(集成 tio)
-- [ ] spring-boot-demo-grpc(集成grpc,配置tls/ssl)参见[ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5)
-- [x] ~~spring-boot-demo-codegen(集成 velocity 自动生成代码)~~
-- [x] ~~spring-boot-demo-graylog(集成 gralog 日志管理)~~
-- [ ] spring-boot-demo-sso(集成单点登录)参见 [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12)
-- [x] ~~spring-boot-demo-ldap (集成 ldap)参见 [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23)~~
-- [x] ~~spring-boot-demo-dynamic-datasource(动态添加数据源,切换数据源)~~
-- [x] ~~spring-boot-demo-ratelimit-guava(单机限流保护API,集成 Guava 的 RateLimiter)~~
-- [x] ~~spring-boot-demo-ratelimit-redis(分布式限流保护API,使用 Redis + lua 脚本实现)~~
-- [x] ~~spring-boot-demo-https(集成 HTTPS)~~
-- [x] ~~spring-boot-demo-elasticsearch-rest-high-level-client(集成 Elasticsearch 7.x 版本,使用官方 rest high level client操作 ES 数据)~~
-- [ ] spring-boot-demo-springbatch(数据处理)
-- [ ] spring-boot-demo-security-justauth(使用 JustAuth 登录 GitHub,使用 Security 管理登录状态)
-- [x] ~~spring-boot-demo-flyway(集成 Flyway,项目启动时初始化数据库表结构,同时支持数据库脚本版本控制)~~
+- [x] ~~demo-helloworld(Helloworld 示例)~~
+- [x] ~~demo-properties(读取配置文件信息)~~
+- [x] ~~demo-actuator(对 Spring boot 的端点监控)~~
+- [x] ~~demo-admin-client(对 Spring boot 可视化管控 客户端)~~
+- [x] ~~demo-admin-server(对 Spring boot 可视化管控 服务端)~~
+- [x] ~~demo-logback(集成 logback 日志)~~
+- [x] ~~demo-log-aop(使用 AOP 拦截请求日志信息)~~
+- [x] ~~demo-exception-handler(统一异常处理)~~
+- [x] ~~demo-template-freemarker(使用模板引擎 - Freemarker)~~
+- [x] ~~demo-template-thymeleaf(使用模板引擎 - thymeleaf)~~
+- [x] ~~demo-template-beetl(使用模板引擎 - beetl)~~
+- [x] ~~demo-template-enjoy(使用模板引擎 - JFinal-Enjoy)~~
+- [x] ~~demo-upload(上传 - 集成本地上传和七牛云上传)~~
+- [x] ~~demo-orm-jdbctemplate(操作 SQL 关系型数据库 - JdbcTemplate)~~
+- [x] ~~demo-orm-jpa(操作 SQL 关系型数据库 - JPA)~~
+- [x] ~~demo-orm-mybatis(操作 SQL 关系型数据库 - mybatis)~~
+- [x] ~~demo-orm-mybatis-mapper-page(操作 SQL 关系型数据库 - 集成mybatis通用Mapper,PageHelper)~~
+- [x] ~~demo-orm-mybatis-plus(操作 SQL 关系型数据库 - 集成mybatis-plus,Mapper操作、ActiveRecord操作)~~
+- [x] ~~demo-orm-beetlsql(操作 SQL 关系型数据库 - beetlSQL)~~
+- [x] ~~demo-cache-redis(使用 redis 进行缓存)~~
+- [x] ~~demo-cache-ehcache(使用 Ehcache 进行缓存)~~
+- [x] ~~demo-email(集成邮件服务)~~
+- [x] ~~demo-task(定时任务 - Task 实现)~~
+- [x] ~~demo-task-quartz(定时任务 - Quartz 实现)~~
+- [x] ~~demo-task-xxl-job(定时任务 - XXL-JOB 实现分布式调度)~~
+- [x] ~~demo-swagger(集成 Swagger 对 API 接口进行测试管理)~~
+- [x] ~~demo-swagger-beauty(集成自定义且更加美观的 Swagger 对 API 接口进行测试管理)~~
+- [x] ~~demo-rbac-security(实现基于 RBAC 的权限模型 - Spring Security)~~
+- [ ] demo-rbac-shiro(实现基于 RBAC 的权限模型 - shiro)
+- [x] ~~demo-session(统一 Session 管理)~~
+- [ ] demo-oauth(OAuth2 认证)
+- [x] ~~demo-social(集成 JustAuth 实现第三方授权验证,实现 QQ、微信、GitHub、谷歌、小米等第三方登录)~~
+- [x] ~~demo-zookeeper(使用 zookeeper 结合AOP实现分布式锁)~~
+- [x] ~~demo-mq-rabbitmq(集成消息中间件 - RabbitMQ)~~
+- [ ] demo-mq-rocketmq(集成消息中间件 - RocketMQ)
+- [x] ~~demo-mq-kafka(集成消息中间件 - Kafka)~~
+- [x] ~~demo-websocket(集成 websocket 服务)~~
+- [x] ~~demo-websocket-socketio(集成 socketio 实现 websocket 服务)~~
+- [x] ~~demo-ureport2 (集成 ureport2 实现自定义的复杂中国式报表引擎)~~
+- [ ] demo-uflo(集成 uflo 实现流程控制引擎)
+- [ ] demo-urule(集成 urule 实现规则引擎)
+- [ ] demo-activiti(集成 Activiti 实现流程控制引擎)
+- [x] ~~demo-async(Spring boot 实现异步调用)~~
+- [x] ~~demo-dubbo(集成 dubbo)~~
+- [x] ~~demo-war(打包成war包)~~
+- [x] ~~demo-elasticsearch(集成 ElasticSearch)~~
+- [x] ~~demo-mongodb(集成 MongoDb)~~
+- [x] ~~demo-neo4j(集成 neo4j 图数据库)~~
+- [x] ~~demo-docker(打包成 docker 镜像)~~
+- [x] ~~demo-multi-datasource-jpa(集成JPA多数据源)~~
+- [x] ~~demo-multi-datasource-mybatis(集成mybatis多数据源)~~
+- [x] ~~demo-sharding-jdbc(集成 sharding-jdbc 实现分库分表)~~
+- [ ] demo-tio(集成 tio)
+- [ ] demo-grpc(集成grpc,配置tls/ssl)参见[ISSUE#5](https://github.com/xkcoding/spring-boot-demo/issues/5)
+- [x] ~~demo-codegen(集成 velocity 自动生成代码)~~
+- [x] ~~demo-graylog(集成 gralog 日志管理)~~
+- [ ] demo-sso(集成单点登录)参见 [ISSUE#12](https://github.com/xkcoding/spring-boot-demo/issues/12)
+- [x] ~~demo-ldap (集成 ldap)参见 [ISSUE#23](https://github.com/xkcoding/spring-boot-demo/issues/23)~~
+- [x] ~~demo-dynamic-datasource(动态添加数据源,切换数据源)~~
+- [x] ~~demo-ratelimit-guava(单机限流保护API,集成 Guava 的 RateLimiter)~~
+- [x] ~~demo-ratelimit-redis(分布式限流保护API,使用 Redis + lua 脚本实现)~~
+- [x] ~~demo-https(集成 HTTPS)~~
+- [x] ~~demo-elasticsearch-rest-high-level-client(集成 Elasticsearch 7.x 版本,使用官方 rest high level client操作 ES 数据)~~
+- [ ] demo-springbatch(数据处理)
+- [ ] demo-security-justauth(使用 JustAuth 登录 GitHub,使用 Security 管理登录状态)
+- [x] ~~demo-flyway(集成 Flyway,项目启动时初始化数据库表结构,同时支持数据库脚本版本控制)~~
## 备注
diff --git a/spring-boot-demo-activiti/.gitignore b/demo-activiti/.gitignore
similarity index 100%
rename from spring-boot-demo-activiti/.gitignore
rename to demo-activiti/.gitignore
diff --git a/demo-activiti/pom.xml b/demo-activiti/pom.xml
new file mode 100644
index 0000000..e82420d
--- /dev/null
+++ b/demo-activiti/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+ demo-activiti
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-activiti
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+
+ org.activiti
+ activiti-spring-boot-starter
+ 7.1.0.M2
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-activiti
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-activiti/src/main/java/com/xkcoding/activiti/SpringBootDemoActivitiApplication.java b/demo-activiti/src/main/java/com/xkcoding/activiti/SpringBootDemoActivitiApplication.java
new file mode 100644
index 0000000..f8d5c99
--- /dev/null
+++ b/demo-activiti/src/main/java/com/xkcoding/activiti/SpringBootDemoActivitiApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.activiti;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-31 22:24
+ */
+@SpringBootApplication
+public class SpringBootDemoActivitiApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoActivitiApplication.class, args);
+ }
+
+}
+
diff --git a/demo-activiti/src/main/java/com/xkcoding/activiti/config/SecurityConfiguration.java b/demo-activiti/src/main/java/com/xkcoding/activiti/config/SecurityConfiguration.java
new file mode 100644
index 0000000..5f46a2a
--- /dev/null
+++ b/demo-activiti/src/main/java/com/xkcoding/activiti/config/SecurityConfiguration.java
@@ -0,0 +1,56 @@
+package com.xkcoding.activiti.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 安全配置类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-07-01 18:40
+ */
+@Slf4j
+@Configuration
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth.userDetailsService(userDetailsService());
+ }
+
+ @Bean
+ protected UserDetailsService myUserDetailsService() {
+ InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
+
+ String[][] usersGroupsAndRoles = {{"salaboy", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"}, {"ryandawsonuk", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"}, {"erdemedeiros", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"}, {"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"}, {"admin", "password", "ROLE_ACTIVITI_ADMIN"}};
+
+ for (String[] user : usersGroupsAndRoles) {
+ List authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
+ log.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
+ inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]), authoritiesStrings.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())));
+ }
+
+
+ return inMemoryUserDetailsManager;
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/demo-activiti/src/main/java/com/xkcoding/activiti/util/SecurityUtil.java b/demo-activiti/src/main/java/com/xkcoding/activiti/util/SecurityUtil.java
new file mode 100755
index 0000000..33a6986
--- /dev/null
+++ b/demo-activiti/src/main/java/com/xkcoding/activiti/util/SecurityUtil.java
@@ -0,0 +1,74 @@
+package com.xkcoding.activiti.util;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+
+/**
+ *
+ * 认证工具
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-07-01 18:38
+ */
+@Component
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class SecurityUtil {
+
+ private final UserDetailsService userDetailsService;
+
+ public void logInAs(String username) {
+
+ UserDetails user = userDetailsService.loadUserByUsername(username);
+ if (user == null) {
+ throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
+ }
+
+ SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return user.getAuthorities();
+ }
+
+ @Override
+ public Object getCredentials() {
+ return user.getPassword();
+ }
+
+ @Override
+ public Object getDetails() {
+ return user;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return user;
+ }
+
+ @Override
+ public boolean isAuthenticated() {
+ return true;
+ }
+
+ @Override
+ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+
+ }
+
+ @Override
+ public String getName() {
+ return user.getUsername();
+ }
+ }));
+ org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
+ }
+}
diff --git a/spring-boot-demo-activiti/src/main/resources/application.yml b/demo-activiti/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-activiti/src/main/resources/application.yml
rename to demo-activiti/src/main/resources/application.yml
diff --git a/spring-boot-demo-activiti/src/main/resources/processes/team01.bpmn b/demo-activiti/src/main/resources/processes/team01.bpmn
similarity index 100%
rename from spring-boot-demo-activiti/src/main/resources/processes/team01.bpmn
rename to demo-activiti/src/main/resources/processes/team01.bpmn
diff --git a/spring-boot-demo-activiti/src/test/java/com/xkcoding/activiti/SpringBootDemoActivitiApplicationTests.java b/demo-activiti/src/test/java/com/xkcoding/activiti/SpringBootDemoActivitiApplicationTests.java
similarity index 100%
rename from spring-boot-demo-activiti/src/test/java/com/xkcoding/activiti/SpringBootDemoActivitiApplicationTests.java
rename to demo-activiti/src/test/java/com/xkcoding/activiti/SpringBootDemoActivitiApplicationTests.java
diff --git a/spring-boot-demo-actuator/.gitignore b/demo-actuator/.gitignore
similarity index 100%
rename from spring-boot-demo-actuator/.gitignore
rename to demo-actuator/.gitignore
diff --git a/spring-boot-demo-actuator/README.md b/demo-actuator/README.md
similarity index 100%
rename from spring-boot-demo-actuator/README.md
rename to demo-actuator/README.md
diff --git a/demo-actuator/pom.xml b/demo-actuator/pom.xml
new file mode 100644
index 0000000..7fc6aef
--- /dev/null
+++ b/demo-actuator/pom.xml
@@ -0,0 +1,64 @@
+
+
+ 4.0.0
+
+ demo-actuator
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-actuator
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+ demo-actuator
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-actuator/src/main/java/com/xkcoding/actuator/SpringBootDemoActuatorApplication.java b/demo-actuator/src/main/java/com/xkcoding/actuator/SpringBootDemoActuatorApplication.java
new file mode 100644
index 0000000..4630c8f
--- /dev/null
+++ b/demo-actuator/src/main/java/com/xkcoding/actuator/SpringBootDemoActuatorApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.actuator;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-9-29 14:27
+ */
+@SpringBootApplication
+public class SpringBootDemoActuatorApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoActuatorApplication.class, args);
+ }
+}
diff --git a/spring-boot-demo-actuator/src/main/resources/application.yml b/demo-actuator/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-actuator/src/main/resources/application.yml
rename to demo-actuator/src/main/resources/application.yml
diff --git a/demo-actuator/src/test/java/com/xkcoding/actuator/SpringBootDemoActuatorApplicationTests.java b/demo-actuator/src/test/java/com/xkcoding/actuator/SpringBootDemoActuatorApplicationTests.java
new file mode 100644
index 0000000..ac2d387
--- /dev/null
+++ b/demo-actuator/src/test/java/com/xkcoding/actuator/SpringBootDemoActuatorApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.actuator;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoActuatorApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/spring-boot-demo-admin/README.md b/demo-admin/README.md
similarity index 100%
rename from spring-boot-demo-admin/README.md
rename to demo-admin/README.md
diff --git a/spring-boot-demo-admin/spring-boot-demo-admin-client/.gitignore b/demo-admin/admin-client/.gitignore
similarity index 100%
rename from spring-boot-demo-admin/spring-boot-demo-admin-client/.gitignore
rename to demo-admin/admin-client/.gitignore
diff --git a/spring-boot-demo-admin/spring-boot-demo-admin-client/README.md b/demo-admin/admin-client/README.md
similarity index 100%
rename from spring-boot-demo-admin/spring-boot-demo-admin-client/README.md
rename to demo-admin/admin-client/README.md
diff --git a/demo-admin/admin-client/pom.xml b/demo-admin/admin-client/pom.xml
new file mode 100644
index 0000000..d5708b4
--- /dev/null
+++ b/demo-admin/admin-client/pom.xml
@@ -0,0 +1,57 @@
+
+
+
+ com.xkcoding
+ demo-admin
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ admin-client
+ 1.0.0-SNAPSHOT
+ jar
+
+ admin-client
+ Demo project for Spring Boot
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ de.codecentric
+ spring-boot-admin-starter-client
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ admin-client
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplication.java b/demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplication.java
new file mode 100644
index 0000000..755bb81
--- /dev/null
+++ b/demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.admin.client;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-8 14:16
+ */
+@SpringBootApplication
+public class SpringBootDemoAdminClientApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoAdminClientApplication.class, args);
+ }
+}
diff --git a/demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/controller/IndexController.java b/demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/controller/IndexController.java
new file mode 100644
index 0000000..98f68ac
--- /dev/null
+++ b/demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/controller/IndexController.java
@@ -0,0 +1,20 @@
+package com.xkcoding.admin.client.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * 首页
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-08 14:15
+ */
+@RestController
+public class IndexController {
+ @GetMapping(value = {"", "/"})
+ public String index() {
+ return "This is a Spring Boot Admin Client.";
+ }
+}
diff --git a/spring-boot-demo-admin/spring-boot-demo-admin-client/src/main/resources/application.yml b/demo-admin/admin-client/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-admin/spring-boot-demo-admin-client/src/main/resources/application.yml
rename to demo-admin/admin-client/src/main/resources/application.yml
diff --git a/demo-admin/admin-client/src/test/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplicationTests.java b/demo-admin/admin-client/src/test/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplicationTests.java
new file mode 100644
index 0000000..283f40a
--- /dev/null
+++ b/demo-admin/admin-client/src/test/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.admin.client;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoAdminClientApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/spring-boot-demo-admin/spring-boot-demo-admin-server/.gitignore b/demo-admin/admin-server/.gitignore
similarity index 100%
rename from spring-boot-demo-admin/spring-boot-demo-admin-server/.gitignore
rename to demo-admin/admin-server/.gitignore
diff --git a/demo-admin/admin-server/README.md b/demo-admin/admin-server/README.md
new file mode 100644
index 0000000..f89cd31
--- /dev/null
+++ b/demo-admin/admin-server/README.md
@@ -0,0 +1,90 @@
+# spring-boot-demo-admin-server
+
+> 本 demo 主要演示了如何搭建一个 Spring Boot Admin 的服务端项目,可视化展示自己客户端项目的运行状态。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-admin-server
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-admin-server
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo-admin
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ de.codecentric
+ spring-boot-admin-starter-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ spring-boot-demo-admin-server
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## SpringBootDemoAdminServerApplication.java
+
+```java
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-08 14:08
+ */
+@EnableAdminServer
+@SpringBootApplication
+public class SpringBootDemoAdminServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoAdminServerApplication.class, args);
+ }
+}
+```
+
+## application.yml
+
+```yaml
+server:
+ port: 8000
+```
+
diff --git a/demo-admin/admin-server/pom.xml b/demo-admin/admin-server/pom.xml
new file mode 100644
index 0000000..0e9f7d6
--- /dev/null
+++ b/demo-admin/admin-server/pom.xml
@@ -0,0 +1,52 @@
+
+
+
+ com.xkcoding
+ demo-admin
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ demo-admin-server
+ 1.0.0-SNAPSHOT
+ jar
+
+ admin-server
+ Demo project for Spring Boot
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ de.codecentric
+ spring-boot-admin-starter-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ admin-server
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-admin/admin-server/src/main/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplication.java b/demo-admin/admin-server/src/main/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplication.java
new file mode 100644
index 0000000..5bf7bad
--- /dev/null
+++ b/demo-admin/admin-server/src/main/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.admin.server;
+
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-08 14:08
+ */
+@EnableAdminServer
+@SpringBootApplication
+public class SpringBootDemoAdminServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoAdminServerApplication.class, args);
+ }
+}
diff --git a/spring-boot-demo-admin/spring-boot-demo-admin-server/src/main/resources/application.yml b/demo-admin/admin-server/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-admin/spring-boot-demo-admin-server/src/main/resources/application.yml
rename to demo-admin/admin-server/src/main/resources/application.yml
diff --git a/demo-admin/admin-server/src/test/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplicationTests.java b/demo-admin/admin-server/src/test/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplicationTests.java
new file mode 100644
index 0000000..d70d4f8
--- /dev/null
+++ b/demo-admin/admin-server/src/test/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.admin.server;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoAdminServerApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/demo-admin/pom.xml b/demo-admin/pom.xml
new file mode 100644
index 0000000..eccaa84
--- /dev/null
+++ b/demo-admin/pom.xml
@@ -0,0 +1,36 @@
+
+
+
+ spring-boot-demo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ demo-admin
+ pom
+
+
+ 2.1.0
+
+
+
+ admin-client
+ admin-server
+
+
+
+
+
+ de.codecentric
+ spring-boot-admin-dependencies
+ ${spring-boot-admin.version}
+ pom
+ import
+
+
+
+
+
diff --git a/spring-boot-demo-async/.gitignore b/demo-async/.gitignore
similarity index 100%
rename from spring-boot-demo-async/.gitignore
rename to demo-async/.gitignore
diff --git a/demo-async/README.md b/demo-async/README.md
new file mode 100644
index 0000000..4561544
--- /dev/null
+++ b/demo-async/README.md
@@ -0,0 +1,257 @@
+# spring-boot-demo-async
+
+> 此 demo 主要演示了 Spring Boot 如何使用原生提供的异步任务支持,实现异步执行任务。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-async
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-async
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-async
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+spring:
+ task:
+ execution:
+ pool:
+ # 最大线程数
+ max-size: 16
+ # 核心线程数
+ core-size: 16
+ # 存活时间
+ keep-alive: 10s
+ # 队列大小
+ queue-capacity: 100
+ # 是否允许核心线程超时
+ allow-core-thread-timeout: true
+ # 线程名称前缀
+ thread-name-prefix: async-task-
+```
+
+## SpringBootDemoAsyncApplication.java
+
+```java
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 10:28
+ */
+@EnableAsync
+@SpringBootApplication
+public class SpringBootDemoAsyncApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoAsyncApplication.class, args);
+ }
+
+}
+```
+
+## TaskFactory.java
+
+```java
+/**
+ *
+ * 任务工厂
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 10:37
+ */
+@Component
+@Slf4j
+public class TaskFactory {
+
+ /**
+ * 模拟5秒的异步任务
+ */
+ @Async
+ public Future asyncTask1() throws InterruptedException {
+ doTask("asyncTask1", 5);
+ return new AsyncResult<>(Boolean.TRUE);
+ }
+
+ /**
+ * 模拟2秒的异步任务
+ */
+ @Async
+ public Future asyncTask2() throws InterruptedException {
+ doTask("asyncTask2", 2);
+ return new AsyncResult<>(Boolean.TRUE);
+ }
+
+ /**
+ * 模拟3秒的异步任务
+ */
+ @Async
+ public Future asyncTask3() throws InterruptedException {
+ doTask("asyncTask3", 3);
+ return new AsyncResult<>(Boolean.TRUE);
+ }
+
+ /**
+ * 模拟5秒的同步任务
+ */
+ public void task1() throws InterruptedException {
+ doTask("task1", 5);
+ }
+
+ /**
+ * 模拟2秒的同步任务
+ */
+ public void task2() throws InterruptedException {
+ doTask("task2", 2);
+ }
+
+ /**
+ * 模拟3秒的同步任务
+ */
+ public void task3() throws InterruptedException {
+ doTask("task3", 3);
+ }
+
+ private void doTask(String taskName, Integer time) throws InterruptedException {
+ log.info("{}开始执行,当前线程名称【{}】", taskName, Thread.currentThread().getName());
+ TimeUnit.SECONDS.sleep(time);
+ log.info("{}执行成功,当前线程名称【{}】", taskName, Thread.currentThread().getName());
+ }
+}
+```
+
+## TaskFactoryTest.java
+
+```java
+/**
+ *
+ * 测试任务
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 10:49
+ */
+@Slf4j
+public class TaskFactoryTest extends SpringBootDemoAsyncApplicationTests {
+ @Autowired
+ private TaskFactory task;
+
+ /**
+ * 测试异步任务
+ */
+ @Test
+ public void asyncTaskTest() throws InterruptedException, ExecutionException {
+ long start = System.currentTimeMillis();
+ Future asyncTask1 = task.asyncTask1();
+ Future asyncTask2 = task.asyncTask2();
+ Future asyncTask3 = task.asyncTask3();
+
+ // 调用 get() 阻塞主线程
+ asyncTask1.get();
+ asyncTask2.get();
+ asyncTask3.get();
+ long end = System.currentTimeMillis();
+
+ log.info("异步任务全部执行结束,总耗时:{} 毫秒", (end - start));
+ }
+
+ /**
+ * 测试同步任务
+ */
+ @Test
+ public void taskTest() throws InterruptedException {
+ long start = System.currentTimeMillis();
+ task.task1();
+ task.task2();
+ task.task3();
+ long end = System.currentTimeMillis();
+
+ log.info("同步任务全部执行结束,总耗时:{} 毫秒", (end - start));
+ }
+}
+```
+
+## 运行结果
+
+### 异步任务
+
+```bash
+2018-12-29 10:57:28.511 INFO 3134 --- [ async-task-3] com.xkcoding.async.task.TaskFactory : asyncTask3开始执行,当前线程名称【async-task-3】
+2018-12-29 10:57:28.511 INFO 3134 --- [ async-task-1] com.xkcoding.async.task.TaskFactory : asyncTask1开始执行,当前线程名称【async-task-1】
+2018-12-29 10:57:28.511 INFO 3134 --- [ async-task-2] com.xkcoding.async.task.TaskFactory : asyncTask2开始执行,当前线程名称【async-task-2】
+2018-12-29 10:57:30.514 INFO 3134 --- [ async-task-2] com.xkcoding.async.task.TaskFactory : asyncTask2执行成功,当前线程名称【async-task-2】
+2018-12-29 10:57:31.516 INFO 3134 --- [ async-task-3] com.xkcoding.async.task.TaskFactory : asyncTask3执行成功,当前线程名称【async-task-3】
+2018-12-29 10:57:33.517 INFO 3134 --- [ async-task-1] com.xkcoding.async.task.TaskFactory : asyncTask1执行成功,当前线程名称【async-task-1】
+2018-12-29 10:57:33.517 INFO 3134 --- [ main] com.xkcoding.async.task.TaskFactoryTest : 异步任务全部执行结束,总耗时:5015 毫秒
+```
+
+### 同步任务
+
+```bash
+2018-12-29 10:55:49.830 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactory : task1开始执行,当前线程名称【main】
+2018-12-29 10:55:54.834 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactory : task1执行成功,当前线程名称【main】
+2018-12-29 10:55:54.835 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactory : task2开始执行,当前线程名称【main】
+2018-12-29 10:55:56.839 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactory : task2执行成功,当前线程名称【main】
+2018-12-29 10:55:56.839 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactory : task3开始执行,当前线程名称【main】
+2018-12-29 10:55:59.843 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactory : task3执行成功,当前线程名称【main】
+2018-12-29 10:55:59.843 INFO 3079 --- [ main] com.xkcoding.async.task.TaskFactoryTest : 同步任务全部执行结束,总耗时:10023 毫秒
+```
+
+## 参考
+
+- Spring Boot 异步任务线程池的配置 参考官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-task-execution-scheduling
diff --git a/demo-async/pom.xml b/demo-async/pom.xml
new file mode 100644
index 0000000..388e38b
--- /dev/null
+++ b/demo-async/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ demo-async
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-async
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-async
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-async/src/main/java/com/xkcoding/async/SpringBootDemoAsyncApplication.java b/demo-async/src/main/java/com/xkcoding/async/SpringBootDemoAsyncApplication.java
new file mode 100644
index 0000000..6d1e8e8
--- /dev/null
+++ b/demo-async/src/main/java/com/xkcoding/async/SpringBootDemoAsyncApplication.java
@@ -0,0 +1,24 @@
+package com.xkcoding.async;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 10:28
+ */
+@EnableAsync
+@SpringBootApplication
+public class SpringBootDemoAsyncApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoAsyncApplication.class, args);
+ }
+
+}
+
diff --git a/demo-async/src/main/java/com/xkcoding/async/task/TaskFactory.java b/demo-async/src/main/java/com/xkcoding/async/task/TaskFactory.java
new file mode 100644
index 0000000..e492110
--- /dev/null
+++ b/demo-async/src/main/java/com/xkcoding/async/task/TaskFactory.java
@@ -0,0 +1,76 @@
+package com.xkcoding.async.task;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.AsyncResult;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ * 任务工厂
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 10:37
+ */
+@Component
+@Slf4j
+public class TaskFactory {
+
+ /**
+ * 模拟5秒的异步任务
+ */
+ @Async
+ public Future asyncTask1() throws InterruptedException {
+ doTask("asyncTask1", 5);
+ return new AsyncResult<>(Boolean.TRUE);
+ }
+
+ /**
+ * 模拟2秒的异步任务
+ */
+ @Async
+ public Future asyncTask2() throws InterruptedException {
+ doTask("asyncTask2", 2);
+ return new AsyncResult<>(Boolean.TRUE);
+ }
+
+ /**
+ * 模拟3秒的异步任务
+ */
+ @Async
+ public Future asyncTask3() throws InterruptedException {
+ doTask("asyncTask3", 3);
+ return new AsyncResult<>(Boolean.TRUE);
+ }
+
+ /**
+ * 模拟5秒的同步任务
+ */
+ public void task1() throws InterruptedException {
+ doTask("task1", 5);
+ }
+
+ /**
+ * 模拟2秒的同步任务
+ */
+ public void task2() throws InterruptedException {
+ doTask("task2", 2);
+ }
+
+ /**
+ * 模拟3秒的同步任务
+ */
+ public void task3() throws InterruptedException {
+ doTask("task3", 3);
+ }
+
+ private void doTask(String taskName, Integer time) throws InterruptedException {
+ log.info("{}开始执行,当前线程名称【{}】", taskName, Thread.currentThread().getName());
+ TimeUnit.SECONDS.sleep(time);
+ log.info("{}执行成功,当前线程名称【{}】", taskName, Thread.currentThread().getName());
+ }
+}
diff --git a/spring-boot-demo-async/src/main/resources/application.yml b/demo-async/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-async/src/main/resources/application.yml
rename to demo-async/src/main/resources/application.yml
diff --git a/spring-boot-demo-async/src/test/java/com/xkcoding/async/SpringBootDemoAsyncApplicationTests.java b/demo-async/src/test/java/com/xkcoding/async/SpringBootDemoAsyncApplicationTests.java
similarity index 100%
rename from spring-boot-demo-async/src/test/java/com/xkcoding/async/SpringBootDemoAsyncApplicationTests.java
rename to demo-async/src/test/java/com/xkcoding/async/SpringBootDemoAsyncApplicationTests.java
diff --git a/demo-async/src/test/java/com/xkcoding/async/task/TaskFactoryTest.java b/demo-async/src/test/java/com/xkcoding/async/task/TaskFactoryTest.java
new file mode 100644
index 0000000..89a226f
--- /dev/null
+++ b/demo-async/src/test/java/com/xkcoding/async/task/TaskFactoryTest.java
@@ -0,0 +1,56 @@
+package com.xkcoding.async.task;
+
+import com.xkcoding.async.SpringBootDemoAsyncApplicationTests;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ *
+ * 测试任务
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 10:49
+ */
+@Slf4j
+public class TaskFactoryTest extends SpringBootDemoAsyncApplicationTests {
+ @Autowired
+ private TaskFactory task;
+
+ /**
+ * 测试异步任务
+ */
+ @Test
+ public void asyncTaskTest() throws InterruptedException, ExecutionException {
+ long start = System.currentTimeMillis();
+ Future asyncTask1 = task.asyncTask1();
+ Future asyncTask2 = task.asyncTask2();
+ Future asyncTask3 = task.asyncTask3();
+
+ // 调用 get() 阻塞主线程
+ asyncTask1.get();
+ asyncTask2.get();
+ asyncTask3.get();
+ long end = System.currentTimeMillis();
+
+ log.info("异步任务全部执行结束,总耗时:{} 毫秒", (end - start));
+ }
+
+ /**
+ * 测试同步任务
+ */
+ @Test
+ public void taskTest() throws InterruptedException {
+ long start = System.currentTimeMillis();
+ task.task1();
+ task.task2();
+ task.task3();
+ long end = System.currentTimeMillis();
+
+ log.info("同步任务全部执行结束,总耗时:{} 毫秒", (end - start));
+ }
+}
diff --git a/spring-boot-demo-cache-ehcache/.gitignore b/demo-cache-ehcache/.gitignore
similarity index 100%
rename from spring-boot-demo-cache-ehcache/.gitignore
rename to demo-cache-ehcache/.gitignore
diff --git a/demo-cache-ehcache/README.md b/demo-cache-ehcache/README.md
new file mode 100644
index 0000000..2d99cc6
--- /dev/null
+++ b/demo-cache-ehcache/README.md
@@ -0,0 +1,286 @@
+# spring-boot-demo-cache-ehcache
+
+> 此 demo 主要演示了 Spring Boot 如何集成 ehcache 使用缓存。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-cache-ehcache
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-cache-ehcache
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+
+ net.sf.ehcache
+ ehcache
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ spring-boot-demo-cache-ehcache
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## SpringBootDemoCacheEhcacheApplication.java
+
+```java
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 17:02
+ */
+@SpringBootApplication
+@EnableCaching
+public class SpringBootDemoCacheEhcacheApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoCacheEhcacheApplication.class, args);
+ }
+}
+```
+
+## application.yml
+
+```yaml
+spring:
+ cache:
+ type: ehcache
+ ehcache:
+ config: classpath:ehcache.xml
+logging:
+ level:
+ com.xkcoding: debug
+```
+
+## ehcache.xml
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## UserServiceImpl.java
+
+```java
+/**
+ *
+ * UserService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 16:54
+ */
+@Service
+@Slf4j
+public class UserServiceImpl implements UserService {
+ /**
+ * 模拟数据库
+ */
+ private static final Map DATABASES = Maps.newConcurrentMap();
+
+ /**
+ * 初始化数据
+ */
+ static {
+ DATABASES.put(1L, new User(1L, "user1"));
+ DATABASES.put(2L, new User(2L, "user2"));
+ DATABASES.put(3L, new User(3L, "user3"));
+ }
+
+ /**
+ * 保存或修改用户
+ *
+ * @param user 用户对象
+ * @return 操作结果
+ */
+ @CachePut(value = "user", key = "#user.id")
+ @Override
+ public User saveOrUpdate(User user) {
+ DATABASES.put(user.getId(), user);
+ log.info("保存用户【user】= {}", user);
+ return user;
+ }
+
+ /**
+ * 获取用户
+ *
+ * @param id key值
+ * @return 返回结果
+ */
+ @Cacheable(value = "user", key = "#id")
+ @Override
+ public User get(Long id) {
+ // 我们假设从数据库读取
+ log.info("查询用户【id】= {}", id);
+ return DATABASES.get(id);
+ }
+
+ /**
+ * 删除
+ *
+ * @param id key值
+ */
+ @CacheEvict(value = "user", key = "#id")
+ @Override
+ public void delete(Long id) {
+ DATABASES.remove(id);
+ log.info("删除用户【id】= {}", id);
+ }
+}
+```
+
+## UserServiceTest.java
+
+```java
+/**
+ *
+ * ehcache缓存测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 16:58
+ */
+@Slf4j
+public class UserServiceTest extends SpringBootDemoCacheEhcacheApplicationTests {
+
+ @Autowired
+ private UserService userService;
+
+ /**
+ * 获取两次,查看日志验证缓存
+ */
+ @Test
+ public void getTwice() {
+ // 模拟查询id为1的用户
+ User user1 = userService.get(1L);
+ log.debug("【user1】= {}", user1);
+
+ // 再次查询
+ User user2 = userService.get(1L);
+ log.debug("【user2】= {}", user2);
+ // 查看日志,只打印一次日志,证明缓存生效
+ }
+
+ /**
+ * 先存,再查询,查看日志验证缓存
+ */
+ @Test
+ public void getAfterSave() {
+ userService.saveOrUpdate(new User(4L, "user4"));
+
+ User user = userService.get(4L);
+ log.debug("【user】= {}", user);
+ // 查看日志,只打印保存用户的日志,查询是未触发查询日志,因此缓存生效
+ }
+
+ /**
+ * 测试删除,查看redis是否存在缓存数据
+ */
+ @Test
+ public void deleteUser() {
+ // 查询一次,使ehcache中存在缓存数据
+ userService.get(1L);
+ // 删除,查看ehcache是否存在缓存数据
+ userService.delete(1L);
+ }
+}
+```
+
+## 参考
+
+- Ehcache 官网:http://www.ehcache.org/documentation/
+- Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-caching-provider-ehcache2
+- 博客:https://juejin.im/post/5b308de9518825748b56ae1d
diff --git a/demo-cache-ehcache/pom.xml b/demo-cache-ehcache/pom.xml
new file mode 100644
index 0000000..8a44447
--- /dev/null
+++ b/demo-cache-ehcache/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ demo-cache-ehcache
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-cache-ehcache
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+
+ net.sf.ehcache
+ ehcache
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ demo-cache-ehcache
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplication.java b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplication.java
new file mode 100644
index 0000000..2fdf43f
--- /dev/null
+++ b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.cache.ehcache;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 17:02
+ */
+@SpringBootApplication
+@EnableCaching
+public class SpringBootDemoCacheEhcacheApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoCacheEhcacheApplication.class, args);
+ }
+}
diff --git a/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/entity/User.java b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/entity/User.java
new file mode 100644
index 0000000..522357b
--- /dev/null
+++ b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/entity/User.java
@@ -0,0 +1,30 @@
+package com.xkcoding.cache.ehcache.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 用户实体
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 16:53
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class User implements Serializable {
+ private static final long serialVersionUID = 2892248514883451461L;
+ /**
+ * 主键id
+ */
+ private Long id;
+ /**
+ * 姓名
+ */
+ private String name;
+}
diff --git a/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/UserService.java b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/UserService.java
new file mode 100644
index 0000000..79fc0f4
--- /dev/null
+++ b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/UserService.java
@@ -0,0 +1,36 @@
+package com.xkcoding.cache.ehcache.service;
+
+import com.xkcoding.cache.ehcache.entity.User;
+
+/**
+ *
+ * UserService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 16:53
+ */
+public interface UserService {
+ /**
+ * 保存或修改用户
+ *
+ * @param user 用户对象
+ * @return 操作结果
+ */
+ User saveOrUpdate(User user);
+
+ /**
+ * 获取用户
+ *
+ * @param id key值
+ * @return 返回结果
+ */
+ User get(Long id);
+
+ /**
+ * 删除
+ *
+ * @param id key值
+ */
+ void delete(Long id);
+}
diff --git a/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/impl/UserServiceImpl.java b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..a013ba7
--- /dev/null
+++ b/demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/impl/UserServiceImpl.java
@@ -0,0 +1,78 @@
+package com.xkcoding.cache.ehcache.service.impl;
+
+import com.google.common.collect.Maps;
+import com.xkcoding.cache.ehcache.entity.User;
+import com.xkcoding.cache.ehcache.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+/**
+ *
+ * UserService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 16:54
+ */
+@Service
+@Slf4j
+public class UserServiceImpl implements UserService {
+ /**
+ * 模拟数据库
+ */
+ private static final Map DATABASES = Maps.newConcurrentMap();
+
+ /**
+ * 初始化数据
+ */
+ static {
+ DATABASES.put(1L, new User(1L, "user1"));
+ DATABASES.put(2L, new User(2L, "user2"));
+ DATABASES.put(3L, new User(3L, "user3"));
+ }
+
+ /**
+ * 保存或修改用户
+ *
+ * @param user 用户对象
+ * @return 操作结果
+ */
+ @CachePut(value = "user", key = "#user.id")
+ @Override
+ public User saveOrUpdate(User user) {
+ DATABASES.put(user.getId(), user);
+ log.info("保存用户【user】= {}", user);
+ return user;
+ }
+
+ /**
+ * 获取用户
+ *
+ * @param id key值
+ * @return 返回结果
+ */
+ @Cacheable(value = "user", key = "#id")
+ @Override
+ public User get(Long id) {
+ // 我们假设从数据库读取
+ log.info("查询用户【id】= {}", id);
+ return DATABASES.get(id);
+ }
+
+ /**
+ * 删除
+ *
+ * @param id key值
+ */
+ @CacheEvict(value = "user", key = "#id")
+ @Override
+ public void delete(Long id) {
+ DATABASES.remove(id);
+ log.info("删除用户【id】= {}", id);
+ }
+}
diff --git a/spring-boot-demo-cache-ehcache/src/main/resources/application.yml b/demo-cache-ehcache/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-cache-ehcache/src/main/resources/application.yml
rename to demo-cache-ehcache/src/main/resources/application.yml
diff --git a/spring-boot-demo-cache-ehcache/src/main/resources/ehcache.xml b/demo-cache-ehcache/src/main/resources/ehcache.xml
similarity index 100%
rename from spring-boot-demo-cache-ehcache/src/main/resources/ehcache.xml
rename to demo-cache-ehcache/src/main/resources/ehcache.xml
diff --git a/spring-boot-demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplicationTests.java b/demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplicationTests.java
similarity index 100%
rename from spring-boot-demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplicationTests.java
rename to demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplicationTests.java
diff --git a/demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/service/UserServiceTest.java b/demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/service/UserServiceTest.java
new file mode 100644
index 0000000..00d3b0f
--- /dev/null
+++ b/demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/service/UserServiceTest.java
@@ -0,0 +1,60 @@
+package com.xkcoding.cache.ehcache.service;
+
+import com.xkcoding.cache.ehcache.SpringBootDemoCacheEhcacheApplicationTests;
+import com.xkcoding.cache.ehcache.entity.User;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ *
+ * ehcache缓存测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-16 16:58
+ */
+@Slf4j
+public class UserServiceTest extends SpringBootDemoCacheEhcacheApplicationTests {
+
+ @Autowired
+ private UserService userService;
+
+ /**
+ * 获取两次,查看日志验证缓存
+ */
+ @Test
+ public void getTwice() {
+ // 模拟查询id为1的用户
+ User user1 = userService.get(1L);
+ log.debug("【user1】= {}", user1);
+
+ // 再次查询
+ User user2 = userService.get(1L);
+ log.debug("【user2】= {}", user2);
+ // 查看日志,只打印一次日志,证明缓存生效
+ }
+
+ /**
+ * 先存,再查询,查看日志验证缓存
+ */
+ @Test
+ public void getAfterSave() {
+ userService.saveOrUpdate(new User(4L, "user4"));
+
+ User user = userService.get(4L);
+ log.debug("【user】= {}", user);
+ // 查看日志,只打印保存用户的日志,查询是未触发查询日志,因此缓存生效
+ }
+
+ /**
+ * 测试删除,查看redis是否存在缓存数据
+ */
+ @Test
+ public void deleteUser() {
+ // 查询一次,使ehcache中存在缓存数据
+ userService.get(1L);
+ // 删除,查看ehcache是否存在缓存数据
+ userService.delete(1L);
+ }
+}
diff --git a/spring-boot-demo-cache-redis/.gitignore b/demo-cache-redis/.gitignore
similarity index 100%
rename from spring-boot-demo-cache-redis/.gitignore
rename to demo-cache-redis/.gitignore
diff --git a/demo-cache-redis/README.md b/demo-cache-redis/README.md
new file mode 100644
index 0000000..7baebf5
--- /dev/null
+++ b/demo-cache-redis/README.md
@@ -0,0 +1,347 @@
+# spring-boot-demo-cache-redis
+
+> 此 demo 主要演示了 Spring Boot 如何整合 redis,操作redis中的数据,并使用redis缓存数据。连接池使用 Lettuce。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-cache-redis
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-cache-redis
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.apache.commons
+ commons-pool2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-json
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.google.guava
+ guava
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-cache-redis
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+spring:
+ redis:
+ host: localhost
+ # 连接超时时间(记得添加单位,Duration)
+ timeout: 10000ms
+ # Redis默认情况下有16个分片,这里配置具体使用的分片
+ # database: 0
+ lettuce:
+ pool:
+ # 连接池最大连接数(使用负值表示没有限制) 默认 8
+ max-active: 8
+ # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
+ max-wait: -1ms
+ # 连接池中的最大空闲连接 默认 8
+ max-idle: 8
+ # 连接池中的最小空闲连接 默认 0
+ min-idle: 0
+ cache:
+ # 一般来说是不用配置的,Spring Cache 会根据依赖的包自行装配
+ type: redis
+logging:
+ level:
+ com.xkcoding: debug
+```
+
+## RedisConfig.java
+
+```java
+/**
+ *
+ * redis配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:41
+ */
+@Configuration
+@AutoConfigureAfter(RedisAutoConfiguration.class)
+@EnableCaching
+public class RedisConfig {
+
+ /**
+ * 默认情况下的模板只能支持RedisTemplate,也就是只能存入字符串,因此支持序列化
+ */
+ @Bean
+ public RedisTemplate redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+ template.setConnectionFactory(redisConnectionFactory);
+ return template;
+ }
+
+ /**
+ * 配置使用注解的时候缓存配置,默认是序列化反序列化的形式,加上此配置则为 json 形式
+ */
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory factory) {
+ // 配置序列化
+ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
+ RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
+
+ return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
+ }
+}
+```
+
+## UserServiceImpl.java
+
+```java
+/**
+ *
+ * UserService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:45
+ */
+@Service
+@Slf4j
+public class UserServiceImpl implements UserService {
+ /**
+ * 模拟数据库
+ */
+ private static final Map DATABASES = Maps.newConcurrentMap();
+
+ /**
+ * 初始化数据
+ */
+ static {
+ DATABASES.put(1L, new User(1L, "user1"));
+ DATABASES.put(2L, new User(2L, "user2"));
+ DATABASES.put(3L, new User(3L, "user3"));
+ }
+
+ /**
+ * 保存或修改用户
+ *
+ * @param user 用户对象
+ * @return 操作结果
+ */
+ @CachePut(value = "user", key = "#user.id")
+ @Override
+ public User saveOrUpdate(User user) {
+ DATABASES.put(user.getId(), user);
+ log.info("保存用户【user】= {}", user);
+ return user;
+ }
+
+ /**
+ * 获取用户
+ *
+ * @param id key值
+ * @return 返回结果
+ */
+ @Cacheable(value = "user", key = "#id")
+ @Override
+ public User get(Long id) {
+ // 我们假设从数据库读取
+ log.info("查询用户【id】= {}", id);
+ return DATABASES.get(id);
+ }
+
+ /**
+ * 删除
+ *
+ * @param id key值
+ */
+ @CacheEvict(value = "user", key = "#id")
+ @Override
+ public void delete(Long id) {
+ DATABASES.remove(id);
+ log.info("删除用户【id】= {}", id);
+ }
+}
+```
+
+## RedisTest.java
+
+> 主要测试使用 `RedisTemplate` 操作 `Redis` 中的数据:
+>
+> - opsForValue:对应 String(字符串)
+> - opsForZSet:对应 ZSet(有序集合)
+> - opsForHash:对应 Hash(哈希)
+> - opsForList:对应 List(列表)
+> - opsForSet:对应 Set(集合)
+> - opsForGeo:** 对应 GEO(地理位置)
+
+```java
+/**
+ *
+ * Redis测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 17:17
+ */
+@Slf4j
+public class RedisTest extends SpringBootDemoCacheRedisApplicationTests {
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+ @Autowired
+ private RedisTemplate redisCacheTemplate;
+
+ /**
+ * 测试 Redis 操作
+ */
+ @Test
+ public void get() {
+ // 测试线程安全,程序结束查看redis中count的值是否为1000
+ ExecutorService executorService = Executors.newFixedThreadPool(1000);
+ IntStream.range(0, 1000).forEach(i -> executorService.execute(() -> stringRedisTemplate.opsForValue().increment("count", 1)));
+
+ stringRedisTemplate.opsForValue().set("k1", "v1");
+ String k1 = stringRedisTemplate.opsForValue().get("k1");
+ log.debug("【k1】= {}", k1);
+
+ // 以下演示整合,具体Redis命令可以参考官方文档
+ String key = "xkcoding:user:1";
+ redisCacheTemplate.opsForValue().set(key, new User(1L, "user1"));
+ // 对应 String(字符串)
+ User user = (User) redisCacheTemplate.opsForValue().get(key);
+ log.debug("【user】= {}", user);
+ }
+}
+
+```
+
+## UserServiceTest.java
+
+> 主要测试使用Redis缓存是否起效
+
+```java
+/**
+ *
+ * Redis - 缓存测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:53
+ */
+@Slf4j
+public class UserServiceTest extends SpringBootDemoCacheRedisApplicationTests {
+ @Autowired
+ private UserService userService;
+
+ /**
+ * 获取两次,查看日志验证缓存
+ */
+ @Test
+ public void getTwice() {
+ // 模拟查询id为1的用户
+ User user1 = userService.get(1L);
+ log.debug("【user1】= {}", user1);
+
+ // 再次查询
+ User user2 = userService.get(1L);
+ log.debug("【user2】= {}", user2);
+ // 查看日志,只打印一次日志,证明缓存生效
+ }
+
+ /**
+ * 先存,再查询,查看日志验证缓存
+ */
+ @Test
+ public void getAfterSave() {
+ userService.saveOrUpdate(new User(4L, "测试中文"));
+
+ User user = userService.get(4L);
+ log.debug("【user】= {}", user);
+ // 查看日志,只打印保存用户的日志,查询是未触发查询日志,因此缓存生效
+ }
+
+ /**
+ * 测试删除,查看redis是否存在缓存数据
+ */
+ @Test
+ public void deleteUser() {
+ // 查询一次,使redis中存在缓存数据
+ userService.get(1L);
+ // 删除,查看redis是否存在缓存数据
+ userService.delete(1L);
+ }
+
+}
+```
+
+## 参考
+
+- spring-data-redis 官方文档:https://docs.spring.io/spring-data/redis/docs/2.0.1.RELEASE/reference/html/
+- redis 文档:https://redis.io/documentation
+- redis 中文文档:http://www.redis.cn/commands.html
diff --git a/demo-cache-redis/pom.xml b/demo-cache-redis/pom.xml
new file mode 100644
index 0000000..5f11beb
--- /dev/null
+++ b/demo-cache-redis/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+ demo-cache-redis
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-cache-redis
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.apache.commons
+ commons-pool2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-json
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.google.guava
+ guava
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-cache-redis
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/spring-boot-demo-cache-redis/src/main/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplication.java b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplication.java
similarity index 100%
rename from spring-boot-demo-cache-redis/src/main/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplication.java
rename to demo-cache-redis/src/main/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplication.java
diff --git a/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/config/RedisConfig.java b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/config/RedisConfig.java
new file mode 100644
index 0000000..dae7aed
--- /dev/null
+++ b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/config/RedisConfig.java
@@ -0,0 +1,56 @@
+package com.xkcoding.cache.redis.config;
+
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.io.Serializable;
+
+/**
+ *
+ * redis配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:41
+ */
+@Configuration
+@AutoConfigureAfter(RedisAutoConfiguration.class)
+@EnableCaching
+public class RedisConfig {
+
+ /**
+ * 默认情况下的模板只能支持RedisTemplate,也就是只能存入字符串,因此支持序列化
+ */
+ @Bean
+ public RedisTemplate redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+ template.setConnectionFactory(redisConnectionFactory);
+ return template;
+ }
+
+ /**
+ * 配置使用注解的时候缓存配置,默认是序列化反序列化的形式,加上此配置则为 json 形式
+ */
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory factory) {
+ // 配置序列化
+ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
+ RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
+
+ return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
+ }
+}
diff --git a/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/entity/User.java b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/entity/User.java
new file mode 100644
index 0000000..f3128e8
--- /dev/null
+++ b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/entity/User.java
@@ -0,0 +1,30 @@
+package com.xkcoding.cache.redis.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 用户实体
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:39
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class User implements Serializable {
+ private static final long serialVersionUID = 2892248514883451461L;
+ /**
+ * 主键id
+ */
+ private Long id;
+ /**
+ * 姓名
+ */
+ private String name;
+}
diff --git a/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/UserService.java b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/UserService.java
new file mode 100644
index 0000000..331901b
--- /dev/null
+++ b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/UserService.java
@@ -0,0 +1,36 @@
+package com.xkcoding.cache.redis.service;
+
+import com.xkcoding.cache.redis.entity.User;
+
+/**
+ *
+ * UserService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:45
+ */
+public interface UserService {
+ /**
+ * 保存或修改用户
+ *
+ * @param user 用户对象
+ * @return 操作结果
+ */
+ User saveOrUpdate(User user);
+
+ /**
+ * 获取用户
+ *
+ * @param id key值
+ * @return 返回结果
+ */
+ User get(Long id);
+
+ /**
+ * 删除
+ *
+ * @param id key值
+ */
+ void delete(Long id);
+}
diff --git a/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/impl/UserServiceImpl.java b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..f7b7d1f
--- /dev/null
+++ b/demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/impl/UserServiceImpl.java
@@ -0,0 +1,78 @@
+package com.xkcoding.cache.redis.service.impl;
+
+import com.google.common.collect.Maps;
+import com.xkcoding.cache.redis.entity.User;
+import com.xkcoding.cache.redis.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+/**
+ *
+ * UserService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:45
+ */
+@Service
+@Slf4j
+public class UserServiceImpl implements UserService {
+ /**
+ * 模拟数据库
+ */
+ private static final Map DATABASES = Maps.newConcurrentMap();
+
+ /**
+ * 初始化数据
+ */
+ static {
+ DATABASES.put(1L, new User(1L, "user1"));
+ DATABASES.put(2L, new User(2L, "user2"));
+ DATABASES.put(3L, new User(3L, "user3"));
+ }
+
+ /**
+ * 保存或修改用户
+ *
+ * @param user 用户对象
+ * @return 操作结果
+ */
+ @CachePut(value = "user", key = "#user.id")
+ @Override
+ public User saveOrUpdate(User user) {
+ DATABASES.put(user.getId(), user);
+ log.info("保存用户【user】= {}", user);
+ return user;
+ }
+
+ /**
+ * 获取用户
+ *
+ * @param id key值
+ * @return 返回结果
+ */
+ @Cacheable(value = "user", key = "#id")
+ @Override
+ public User get(Long id) {
+ // 我们假设从数据库读取
+ log.info("查询用户【id】= {}", id);
+ return DATABASES.get(id);
+ }
+
+ /**
+ * 删除
+ *
+ * @param id key值
+ */
+ @CacheEvict(value = "user", key = "#id")
+ @Override
+ public void delete(Long id) {
+ DATABASES.remove(id);
+ log.info("删除用户【id】= {}", id);
+ }
+}
diff --git a/spring-boot-demo-cache-redis/src/main/resources/application.yml b/demo-cache-redis/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-cache-redis/src/main/resources/application.yml
rename to demo-cache-redis/src/main/resources/application.yml
diff --git a/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/RedisTest.java b/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/RedisTest.java
new file mode 100644
index 0000000..8389ea5
--- /dev/null
+++ b/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/RedisTest.java
@@ -0,0 +1,52 @@
+package com.xkcoding.cache.redis;
+
+import com.xkcoding.cache.redis.entity.User;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.io.Serializable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.IntStream;
+
+/**
+ *
+ * Redis测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 17:17
+ */
+@Slf4j
+public class RedisTest extends SpringBootDemoCacheRedisApplicationTests {
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+ @Autowired
+ private RedisTemplate redisCacheTemplate;
+
+ /**
+ * 测试 Redis 操作
+ */
+ @Test
+ public void get() {
+ // 测试线程安全,程序结束查看redis中count的值是否为1000
+ ExecutorService executorService = Executors.newFixedThreadPool(1000);
+ IntStream.range(0, 1000).forEach(i -> executorService.execute(() -> stringRedisTemplate.opsForValue().increment("count", 1)));
+
+ stringRedisTemplate.opsForValue().set("k1", "v1");
+ String k1 = stringRedisTemplate.opsForValue().get("k1");
+ log.debug("【k1】= {}", k1);
+
+ // 以下演示整合,具体Redis命令可以参考官方文档
+ String key = "xkcoding:user:1";
+ redisCacheTemplate.opsForValue().set(key, new User(1L, "user1"));
+ // 对应 String(字符串)
+ User user = (User) redisCacheTemplate.opsForValue().get(key);
+ log.debug("【user】= {}", user);
+ }
+}
diff --git a/spring-boot-demo-cache-redis/src/test/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplicationTests.java b/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplicationTests.java
similarity index 100%
rename from spring-boot-demo-cache-redis/src/test/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplicationTests.java
rename to demo-cache-redis/src/test/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplicationTests.java
diff --git a/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/service/UserServiceTest.java b/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/service/UserServiceTest.java
new file mode 100644
index 0000000..3318787
--- /dev/null
+++ b/demo-cache-redis/src/test/java/com/xkcoding/cache/redis/service/UserServiceTest.java
@@ -0,0 +1,60 @@
+package com.xkcoding.cache.redis.service;
+
+import com.xkcoding.cache.redis.SpringBootDemoCacheRedisApplicationTests;
+import com.xkcoding.cache.redis.entity.User;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ *
+ * Redis - 缓存测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-15 16:53
+ */
+@Slf4j
+public class UserServiceTest extends SpringBootDemoCacheRedisApplicationTests {
+ @Autowired
+ private UserService userService;
+
+ /**
+ * 获取两次,查看日志验证缓存
+ */
+ @Test
+ public void getTwice() {
+ // 模拟查询id为1的用户
+ User user1 = userService.get(1L);
+ log.debug("【user1】= {}", user1);
+
+ // 再次查询
+ User user2 = userService.get(1L);
+ log.debug("【user2】= {}", user2);
+ // 查看日志,只打印一次日志,证明缓存生效
+ }
+
+ /**
+ * 先存,再查询,查看日志验证缓存
+ */
+ @Test
+ public void getAfterSave() {
+ userService.saveOrUpdate(new User(4L, "测试中文"));
+
+ User user = userService.get(4L);
+ log.debug("【user】= {}", user);
+ // 查看日志,只打印保存用户的日志,查询是未触发查询日志,因此缓存生效
+ }
+
+ /**
+ * 测试删除,查看redis是否存在缓存数据
+ */
+ @Test
+ public void deleteUser() {
+ // 查询一次,使redis中存在缓存数据
+ userService.get(1L);
+ // 删除,查看redis是否存在缓存数据
+ userService.delete(1L);
+ }
+
+}
diff --git a/spring-boot-demo-codegen/.gitignore b/demo-codegen/.gitignore
similarity index 100%
rename from spring-boot-demo-codegen/.gitignore
rename to demo-codegen/.gitignore
diff --git a/demo-codegen/README.md b/demo-codegen/README.md
new file mode 100644
index 0000000..ce635c2
--- /dev/null
+++ b/demo-codegen/README.md
@@ -0,0 +1,410 @@
+# spring-boot-demo-codegen
+
+> 此 demo 主要演示了 Spring Boot 使用**模板技术**生成代码,并提供前端页面,可生成 Entity/Mapper/Service/Controller 等代码。
+
+## 1. 主要功能
+
+1. 使用 `velocity` 代码生成
+2. 暂时支持mysql数据库的代码生成
+3. 提供前端页面展示,并下载代码压缩包
+
+> 注意:① Entity里使用lombok,简化代码 ② Mapper 和 Service 层集成 Mybatis-Plus 简化代码
+
+## 2. 运行
+
+1. 运行 `SpringBootDemoCodegenApplication` 启动项目
+2. 打开浏览器,输入 http://localhost:8080/demo/index.html
+3. 输入查询条件,生成代码
+
+## 3. 关键代码
+
+### 3.1. pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-codegen
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-codegen
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-undertow
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.zaxxer
+ HikariCP
+
+
+
+
+ org.apache.velocity
+ velocity
+ 1.7
+
+
+
+ org.apache.commons
+ commons-text
+ 1.6
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-codegen
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+### 3.2. 代码生成器配置
+
+```properties
+#代码生成器,配置信息
+mainPath=com.xkcoding
+#包名
+package=com.xkcoding
+moduleName=generator
+#作者
+author=Yangkai.Shen
+#表前缀(类名不会包含表前缀)
+tablePrefix=tb_
+#类型转换,配置信息
+tinyint=Integer
+smallint=Integer
+mediumint=Integer
+int=Integer
+integer=Integer
+bigint=Long
+float=Float
+double=Double
+decimal=BigDecimal
+bit=Boolean
+char=String
+varchar=String
+tinytext=String
+text=String
+mediumtext=String
+longtext=String
+date=LocalDateTime
+datetime=LocalDateTime
+timestamp=LocalDateTime
+```
+
+### 3.3. CodeGenUtil.java
+
+```java
+/**
+ *
+ * 代码生成器 工具类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 09:27
+ */
+@Slf4j
+@UtilityClass
+public class CodeGenUtil {
+
+ private final String ENTITY_JAVA_VM = "Entity.java.vm";
+ private final String MAPPER_JAVA_VM = "Mapper.java.vm";
+ private final String SERVICE_JAVA_VM = "Service.java.vm";
+ private final String SERVICE_IMPL_JAVA_VM = "ServiceImpl.java.vm";
+ private final String CONTROLLER_JAVA_VM = "Controller.java.vm";
+ private final String MAPPER_XML_VM = "Mapper.xml.vm";
+ private final String API_JS_VM = "api.js.vm";
+
+ private List getTemplates() {
+ List templates = new ArrayList<>();
+ templates.add("template/Entity.java.vm");
+ templates.add("template/Mapper.java.vm");
+ templates.add("template/Mapper.xml.vm");
+ templates.add("template/Service.java.vm");
+ templates.add("template/ServiceImpl.java.vm");
+ templates.add("template/Controller.java.vm");
+
+ templates.add("template/api.js.vm");
+ return templates;
+ }
+
+ /**
+ * 生成代码
+ */
+ public void generatorCode(GenConfig genConfig, Entity table, List columns, ZipOutputStream zip) {
+ //配置信息
+ Props props = getConfig();
+ boolean hasBigDecimal = false;
+ //表信息
+ TableEntity tableEntity = new TableEntity();
+ tableEntity.setTableName(table.getStr("tableName"));
+
+ if (StrUtil.isNotBlank(genConfig.getComments())) {
+ tableEntity.setComments(genConfig.getComments());
+ } else {
+ tableEntity.setComments(table.getStr("tableComment"));
+ }
+
+ String tablePrefix;
+ if (StrUtil.isNotBlank(genConfig.getTablePrefix())) {
+ tablePrefix = genConfig.getTablePrefix();
+ } else {
+ tablePrefix = props.getStr("tablePrefix");
+ }
+
+ //表名转换成Java类名
+ String className = tableToJava(tableEntity.getTableName(), tablePrefix);
+ tableEntity.setCaseClassName(className);
+ tableEntity.setLowerClassName(StrUtil.lowerFirst(className));
+
+ //列信息
+ List columnList = Lists.newArrayList();
+ for (Entity column : columns) {
+ ColumnEntity columnEntity = new ColumnEntity();
+ columnEntity.setColumnName(column.getStr("columnName"));
+ columnEntity.setDataType(column.getStr("dataType"));
+ columnEntity.setComments(column.getStr("columnComment"));
+ columnEntity.setExtra(column.getStr("extra"));
+
+ //列名转换成Java属性名
+ String attrName = columnToJava(columnEntity.getColumnName());
+ columnEntity.setCaseAttrName(attrName);
+ columnEntity.setLowerAttrName(StrUtil.lowerFirst(attrName));
+
+ //列的数据类型,转换成Java类型
+ String attrType = props.getStr(columnEntity.getDataType(), "unknownType");
+ columnEntity.setAttrType(attrType);
+ if (!hasBigDecimal && "BigDecimal".equals(attrType)) {
+ hasBigDecimal = true;
+ }
+ //是否主键
+ if ("PRI".equalsIgnoreCase(column.getStr("columnKey")) && tableEntity.getPk() == null) {
+ tableEntity.setPk(columnEntity);
+ }
+
+ columnList.add(columnEntity);
+ }
+ tableEntity.setColumns(columnList);
+
+ //没主键,则第一个字段为主键
+ if (tableEntity.getPk() == null) {
+ tableEntity.setPk(tableEntity.getColumns().get(0));
+ }
+
+ //设置velocity资源加载器
+ Properties prop = new Properties();
+ prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+ Velocity.init(prop);
+ //封装模板数据
+ Map map = new HashMap<>(16);
+ map.put("tableName", tableEntity.getTableName());
+ map.put("pk", tableEntity.getPk());
+ map.put("className", tableEntity.getCaseClassName());
+ map.put("classname", tableEntity.getLowerClassName());
+ map.put("pathName", tableEntity.getLowerClassName().toLowerCase());
+ map.put("columns", tableEntity.getColumns());
+ map.put("hasBigDecimal", hasBigDecimal);
+ map.put("datetime", DateUtil.now());
+ map.put("year", DateUtil.year(new Date()));
+
+ if (StrUtil.isNotBlank(genConfig.getComments())) {
+ map.put("comments", genConfig.getComments());
+ } else {
+ map.put("comments", tableEntity.getComments());
+ }
+
+ if (StrUtil.isNotBlank(genConfig.getAuthor())) {
+ map.put("author", genConfig.getAuthor());
+ } else {
+ map.put("author", props.getStr("author"));
+ }
+
+ if (StrUtil.isNotBlank(genConfig.getModuleName())) {
+ map.put("moduleName", genConfig.getModuleName());
+ } else {
+ map.put("moduleName", props.getStr("moduleName"));
+ }
+
+ if (StrUtil.isNotBlank(genConfig.getPackageName())) {
+ map.put("package", genConfig.getPackageName());
+ map.put("mainPath", genConfig.getPackageName());
+ } else {
+ map.put("package", props.getStr("package"));
+ map.put("mainPath", props.getStr("mainPath"));
+ }
+ VelocityContext context = new VelocityContext(map);
+
+ //获取模板列表
+ List templates = getTemplates();
+ for (String template : templates) {
+ //渲染模板
+ StringWriter sw = new StringWriter();
+ Template tpl = Velocity.getTemplate(template, CharsetUtil.UTF_8);
+ tpl.merge(context, sw);
+
+ try {
+ //添加到zip
+ zip.putNextEntry(new ZipEntry(Objects.requireNonNull(getFileName(template, tableEntity.getCaseClassName(), map
+ .get("package")
+ .toString(), map.get("moduleName").toString()))));
+ IoUtil.write(zip, StandardCharsets.UTF_8, false, sw.toString());
+ IoUtil.close(sw);
+ zip.closeEntry();
+ } catch (IOException e) {
+ throw new RuntimeException("渲染模板失败,表名:" + tableEntity.getTableName(), e);
+ }
+ }
+ }
+
+
+ /**
+ * 列名转换成Java属性名
+ */
+ private String columnToJava(String columnName) {
+ return WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", "");
+ }
+
+ /**
+ * 表名转换成Java类名
+ */
+ private String tableToJava(String tableName, String tablePrefix) {
+ if (StrUtil.isNotBlank(tablePrefix)) {
+ tableName = tableName.replaceFirst(tablePrefix, "");
+ }
+ return columnToJava(tableName);
+ }
+
+ /**
+ * 获取配置信息
+ */
+ private Props getConfig() {
+ Props props = new Props("generator.properties");
+ props.autoLoad(true);
+ return props;
+ }
+
+ /**
+ * 获取文件名
+ */
+ private String getFileName(String template, String className, String packageName, String moduleName) {
+ // 包路径
+ String packagePath = GenConstants.SIGNATURE + File.separator + "src" + File.separator + "main" + File.separator + "java" + File.separator;
+ // 资源路径
+ String resourcePath = GenConstants.SIGNATURE + File.separator + "src" + File.separator + "main" + File.separator + "resources" + File.separator;
+ // api路径
+ String apiPath = GenConstants.SIGNATURE + File.separator + "api" + File.separator;
+
+ if (StrUtil.isNotBlank(packageName)) {
+ packagePath += packageName.replace(".", File.separator) + File.separator + moduleName + File.separator;
+ }
+
+ if (template.contains(ENTITY_JAVA_VM)) {
+ return packagePath + "entity" + File.separator + className + ".java";
+ }
+
+ if (template.contains(MAPPER_JAVA_VM)) {
+ return packagePath + "mapper" + File.separator + className + "Mapper.java";
+ }
+
+ if (template.contains(SERVICE_JAVA_VM)) {
+ return packagePath + "service" + File.separator + className + "Service.java";
+ }
+
+ if (template.contains(SERVICE_IMPL_JAVA_VM)) {
+ return packagePath + "service" + File.separator + "impl" + File.separator + className + "ServiceImpl.java";
+ }
+
+ if (template.contains(CONTROLLER_JAVA_VM)) {
+ return packagePath + "controller" + File.separator + className + "Controller.java";
+ }
+
+ if (template.contains(MAPPER_XML_VM)) {
+ return resourcePath + "mapper" + File.separator + className + "Mapper.xml";
+ }
+
+ if (template.contains(API_JS_VM)) {
+ return apiPath + className.toLowerCase() + ".js";
+ }
+
+ return null;
+ }
+}
+```
+
+### 3.4. 其余代码参见demo
+
+## 4. 演示
+
+
+
+ 您的浏览器版本过低,不支持播放视频演示,可下载演示视频观看,https://static.xkcoding.com/code/spring-boot-demo/codegen/codegen.mp4
+
+
+## 5. 参考
+
+- [基于人人开源 自动构建项目_V1](https://qq343509740.gitee.io/2018/12/20/%E7%AC%94%E8%AE%B0/%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE/%E5%9F%BA%E4%BA%8E%E4%BA%BA%E4%BA%BA%E5%BC%80%E6%BA%90%20%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE_V1/)
+
+- [Mybatis-Plus代码生成器](https://mybatis.plus/guide/generator.html#%E6%B7%BB%E5%8A%A0%E4%BE%9D%E8%B5%96)
diff --git a/demo-codegen/pom.xml b/demo-codegen/pom.xml
new file mode 100644
index 0000000..eeefe12
--- /dev/null
+++ b/demo-codegen/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ demo-codegen
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-codegen
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-undertow
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.zaxxer
+ HikariCP
+
+
+
+
+ org.apache.velocity
+ velocity-engine-core
+ 2.1
+
+
+
+ org.apache.commons
+ commons-text
+ 1.6
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-codegen
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/SpringBootDemoCodegenApplication.java b/demo-codegen/src/main/java/com/xkcoding/codegen/SpringBootDemoCodegenApplication.java
new file mode 100644
index 0000000..6d3b118
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/SpringBootDemoCodegenApplication.java
@@ -0,0 +1,21 @@
+package com.xkcoding.codegen;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 09:10
+ */
+@SpringBootApplication
+public class SpringBootDemoCodegenApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoCodegenApplication.class, args);
+ }
+
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/common/IResultCode.java b/demo-codegen/src/main/java/com/xkcoding/codegen/common/IResultCode.java
new file mode 100644
index 0000000..d2b1108
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/common/IResultCode.java
@@ -0,0 +1,25 @@
+package com.xkcoding.codegen.common;
+
+/**
+ *
+ * 统一状态码接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-21 16:28
+ */
+public interface IResultCode {
+ /**
+ * 获取状态码
+ *
+ * @return 状态码
+ */
+ Integer getCode();
+
+ /**
+ * 获取返回消息
+ *
+ * @return 返回消息
+ */
+ String getMessage();
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/common/PageResult.java b/demo-codegen/src/main/java/com/xkcoding/codegen/common/PageResult.java
new file mode 100644
index 0000000..f05de4a
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/common/PageResult.java
@@ -0,0 +1,38 @@
+package com.xkcoding.codegen.common;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ *
+ * 分页结果集
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 11:24
+ */
+@Data
+@AllArgsConstructor
+public class PageResult {
+ /**
+ * 总条数
+ */
+ private Long total;
+
+ /**
+ * 页码
+ */
+ private int pageNumber;
+
+ /**
+ * 每页结果数
+ */
+ private int pageSize;
+
+ /**
+ * 结果集
+ */
+ private List list;
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/common/R.java b/demo-codegen/src/main/java/com/xkcoding/codegen/common/R.java
new file mode 100644
index 0000000..af2a338
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/common/R.java
@@ -0,0 +1,90 @@
+package com.xkcoding.codegen.common;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ *
+ * 统一API对象返回
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:13
+ */
+@Data
+@NoArgsConstructor
+public class R {
+ /**
+ * 状态码
+ */
+ private Integer code;
+
+ /**
+ * 返回消息
+ */
+ private String message;
+
+ /**
+ * 状态
+ */
+ private boolean status;
+
+ /**
+ * 返回数据
+ */
+ private T data;
+
+ public R(Integer code, String message, boolean status, T data) {
+ this.code = code;
+ this.message = message;
+ this.status = status;
+ this.data = data;
+ }
+
+ public R(IResultCode resultCode, boolean status, T data) {
+ this.code = resultCode.getCode();
+ this.message = resultCode.getMessage();
+ this.status = status;
+ this.data = data;
+ }
+
+ public R(IResultCode resultCode, boolean status) {
+ this.code = resultCode.getCode();
+ this.message = resultCode.getMessage();
+ this.status = status;
+ this.data = null;
+ }
+
+ public static R success() {
+ return new R<>(ResultCode.OK, true);
+ }
+
+ public static R message(String message) {
+ return new R<>(ResultCode.OK.getCode(), message, true, null);
+ }
+
+ public static R success(T data) {
+ return new R<>(ResultCode.OK, true, data);
+ }
+
+ public static R fail() {
+ return new R<>(ResultCode.ERROR, false);
+ }
+
+ public static R fail(IResultCode resultCode) {
+ return new R<>(resultCode, false);
+ }
+
+ public static R fail(Integer code, String message) {
+ return new R<>(code, message, false, null);
+ }
+
+ public static R fail(IResultCode resultCode, T data) {
+ return new R<>(resultCode, false, data);
+ }
+
+ public static R fail(Integer code, String message, T data) {
+ return new R<>(code, message, false, data);
+ }
+
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/common/ResultCode.java b/demo-codegen/src/main/java/com/xkcoding/codegen/common/ResultCode.java
new file mode 100644
index 0000000..5f06f3e
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/common/ResultCode.java
@@ -0,0 +1,39 @@
+package com.xkcoding.codegen.common;
+
+import lombok.Getter;
+
+/**
+ *
+ * 通用状态枚举
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:13
+ */
+@Getter
+public enum ResultCode implements IResultCode {
+ /**
+ * 成功
+ */
+ OK(200, "成功"),
+ /**
+ * 失败
+ */
+ ERROR(500, "失败");
+
+ /**
+ * 返回码
+ */
+ private Integer code;
+
+ /**
+ * 返回消息
+ */
+ private String message;
+
+ ResultCode(Integer code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/constants/GenConstants.java b/demo-codegen/src/main/java/com/xkcoding/codegen/constants/GenConstants.java
new file mode 100644
index 0000000..6959cbc
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/constants/GenConstants.java
@@ -0,0 +1,16 @@
+package com.xkcoding.codegen.constants;
+
+/**
+ *
+ * 常量池
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:04
+ */
+public interface GenConstants {
+ /**
+ * 签名
+ */
+ String SIGNATURE = "xkcoding代码生成";
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/controller/CodeGenController.java b/demo-codegen/src/main/java/com/xkcoding/codegen/controller/CodeGenController.java
new file mode 100755
index 0000000..879e546
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/controller/CodeGenController.java
@@ -0,0 +1,55 @@
+package com.xkcoding.codegen.controller;
+
+import cn.hutool.core.io.IoUtil;
+import com.xkcoding.codegen.common.R;
+import com.xkcoding.codegen.entity.GenConfig;
+import com.xkcoding.codegen.entity.TableRequest;
+import com.xkcoding.codegen.service.CodeGenService;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ *
+ * 代码生成器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:11
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/generator")
+public class CodeGenController {
+ private final CodeGenService codeGenService;
+
+ /**
+ * 列表
+ *
+ * @param request 参数集
+ * @return 数据库表
+ */
+ @GetMapping("/table")
+ public R listTables(TableRequest request) {
+ return R.success(codeGenService.listTables(request));
+ }
+
+ /**
+ * 生成代码
+ */
+ @SneakyThrows
+ @PostMapping("")
+ public void generatorCode(@RequestBody GenConfig genConfig, HttpServletResponse response) {
+ byte[] data = codeGenService.generatorCode(genConfig);
+
+ response.reset();
+ response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=%s.zip", genConfig.getTableName()));
+ response.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(data.length));
+ response.setContentType("application/octet-stream; charset=UTF-8");
+
+ IoUtil.write(response.getOutputStream(), Boolean.TRUE, data);
+ }
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/entity/ColumnEntity.java b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/ColumnEntity.java
new file mode 100755
index 0000000..1c1e1aa
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/ColumnEntity.java
@@ -0,0 +1,47 @@
+package com.xkcoding.codegen.entity;
+
+import lombok.Data;
+
+/**
+ *
+ * 列属性: https://blog.csdn.net/lkforce/article/details/79557482
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 09:46
+ */
+@Data
+public class ColumnEntity {
+ /**
+ * 列表
+ */
+ private String columnName;
+ /**
+ * 数据类型
+ */
+ private String dataType;
+ /**
+ * 备注
+ */
+ private String comments;
+ /**
+ * 驼峰属性
+ */
+ private String caseAttrName;
+ /**
+ * 普通属性
+ */
+ private String lowerAttrName;
+ /**
+ * 属性类型
+ */
+ private String attrType;
+ /**
+ * jdbc类型
+ */
+ private String jdbcType;
+ /**
+ * 其他信息
+ */
+ private String extra;
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/entity/GenConfig.java b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/GenConfig.java
new file mode 100644
index 0000000..107d136
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/GenConfig.java
@@ -0,0 +1,43 @@
+package com.xkcoding.codegen.entity;
+
+import lombok.Data;
+
+/**
+ *
+ * 生成配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 09:47
+ */
+@Data
+public class GenConfig {
+ /**
+ * 请求参数
+ */
+ private TableRequest request;
+ /**
+ * 包名
+ */
+ private String packageName;
+ /**
+ * 作者
+ */
+ private String author;
+ /**
+ * 模块名称
+ */
+ private String moduleName;
+ /**
+ * 表前缀
+ */
+ private String tablePrefix;
+ /**
+ * 表名称
+ */
+ private String tableName;
+ /**
+ * 表备注
+ */
+ private String comments;
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableEntity.java b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableEntity.java
new file mode 100755
index 0000000..1feb7c2
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableEntity.java
@@ -0,0 +1,41 @@
+package com.xkcoding.codegen.entity;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ *
+ * 表属性: https://blog.csdn.net/lkforce/article/details/79557482
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 09:47
+ */
+@Data
+public class TableEntity {
+ /**
+ * 名称
+ */
+ private String tableName;
+ /**
+ * 备注
+ */
+ private String comments;
+ /**
+ * 主键
+ */
+ private ColumnEntity pk;
+ /**
+ * 列名
+ */
+ private List columns;
+ /**
+ * 驼峰类型
+ */
+ private String caseClassName;
+ /**
+ * 普通类型
+ */
+ private String lowerClassName;
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableRequest.java b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableRequest.java
new file mode 100644
index 0000000..f091c7f
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableRequest.java
@@ -0,0 +1,43 @@
+package com.xkcoding.codegen.entity;
+
+import lombok.Data;
+
+/**
+ *
+ * 表格请求参数
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:24
+ */
+@Data
+public class TableRequest {
+ /**
+ * 当前页
+ */
+ private Integer currentPage;
+ /**
+ * 每页条数
+ */
+ private Integer pageSize;
+ /**
+ * jdbc-前缀
+ */
+ private String prepend;
+ /**
+ * jdbc-url
+ */
+ private String url;
+ /**
+ * 用户名
+ */
+ private String username;
+ /**
+ * 密码
+ */
+ private String password;
+ /**
+ * 表名
+ */
+ private String tableName;
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/service/CodeGenService.java b/demo-codegen/src/main/java/com/xkcoding/codegen/service/CodeGenService.java
new file mode 100644
index 0000000..c71af32
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/service/CodeGenService.java
@@ -0,0 +1,32 @@
+package com.xkcoding.codegen.service;
+
+import cn.hutool.db.Entity;
+import com.xkcoding.codegen.common.PageResult;
+import com.xkcoding.codegen.entity.GenConfig;
+import com.xkcoding.codegen.entity.TableRequest;
+
+/**
+ *
+ * 代码生成器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:15
+ */
+public interface CodeGenService {
+ /**
+ * 生成代码
+ *
+ * @param genConfig 生成配置
+ * @return 代码压缩文件
+ */
+ byte[] generatorCode(GenConfig genConfig);
+
+ /**
+ * 分页查询表信息
+ *
+ * @param request 请求参数
+ * @return 表名分页信息
+ */
+ PageResult listTables(TableRequest request);
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/service/impl/CodeGenServiceImpl.java b/demo-codegen/src/main/java/com/xkcoding/codegen/service/impl/CodeGenServiceImpl.java
new file mode 100755
index 0000000..b39bd19
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/service/impl/CodeGenServiceImpl.java
@@ -0,0 +1,130 @@
+package com.xkcoding.codegen.service.impl;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.db.Db;
+import cn.hutool.db.Entity;
+import cn.hutool.db.Page;
+import com.xkcoding.codegen.common.PageResult;
+import com.xkcoding.codegen.entity.GenConfig;
+import com.xkcoding.codegen.entity.TableRequest;
+import com.xkcoding.codegen.service.CodeGenService;
+import com.xkcoding.codegen.utils.CodeGenUtil;
+import com.xkcoding.codegen.utils.DbUtil;
+import com.zaxxer.hikari.HikariDataSource;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.zip.ZipOutputStream;
+
+/**
+ *
+ * 代码生成器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:15
+ */
+@Service
+@AllArgsConstructor
+public class CodeGenServiceImpl implements CodeGenService {
+ private final String TABLE_SQL_TEMPLATE = "select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables where table_schema = (select database()) %s order by create_time desc";
+
+ private final String COLUMN_SQL_TEMPLATE = "select column_name columnName, data_type dataType, column_comment columnComment, column_key columnKey, extra from information_schema.columns where table_name = ? and table_schema = (select database()) order by ordinal_position";
+
+ private final String COUNT_SQL_TEMPLATE = "select count(1) from (%s)tmp";
+
+ private final String PAGE_SQL_TEMPLATE = " limit ?,?";
+
+ /**
+ * 分页查询表信息
+ *
+ * @param request 请求参数
+ * @return 表名分页信息
+ */
+ @Override
+ @SneakyThrows
+ public PageResult listTables(TableRequest request) {
+ HikariDataSource dataSource = DbUtil.buildFromTableRequest(request);
+ Db db = new Db(dataSource);
+
+ Page page = new Page(request.getCurrentPage(), request.getPageSize());
+ int start = page.getStartPosition();
+ int pageSize = page.getPageSize();
+
+ String paramSql = StrUtil.EMPTY;
+ if (StrUtil.isNotBlank(request.getTableName())) {
+ paramSql = "and table_name like concat('%', ?, '%')";
+ }
+ String sql = String.format(TABLE_SQL_TEMPLATE, paramSql);
+ String countSql = String.format(COUNT_SQL_TEMPLATE, sql);
+
+ List query;
+ BigDecimal count;
+ if (StrUtil.isNotBlank(request.getTableName())) {
+ query = db.query(sql + PAGE_SQL_TEMPLATE, request.getTableName(), start, pageSize);
+ count = (BigDecimal) db.queryNumber(countSql, request.getTableName());
+ } else {
+ query = db.query(sql + PAGE_SQL_TEMPLATE, start, pageSize);
+ count = (BigDecimal) db.queryNumber(countSql);
+ }
+
+ PageResult pageResult = new PageResult<>(count.longValue(), page.getPageNumber(), page.getPageSize(), query);
+
+ dataSource.close();
+ return pageResult;
+ }
+
+ /**
+ * 生成代码
+ *
+ * @param genConfig 生成配置
+ * @return 代码压缩文件
+ */
+ @Override
+ public byte[] generatorCode(GenConfig genConfig) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ZipOutputStream zip = new ZipOutputStream(outputStream);
+
+ //查询表信息
+ Entity table = queryTable(genConfig.getRequest());
+ //查询列信息
+ List columns = queryColumns(genConfig.getRequest());
+ //生成代码
+ CodeGenUtil.generatorCode(genConfig, table, columns, zip);
+ IoUtil.close(zip);
+ return outputStream.toByteArray();
+ }
+
+ @SneakyThrows
+ private Entity queryTable(TableRequest request) {
+ HikariDataSource dataSource = DbUtil.buildFromTableRequest(request);
+ Db db = new Db(dataSource);
+
+ String paramSql = StrUtil.EMPTY;
+ if (StrUtil.isNotBlank(request.getTableName())) {
+ paramSql = "and table_name = ?";
+ }
+ String sql = String.format(TABLE_SQL_TEMPLATE, paramSql);
+ Entity entity = db.queryOne(sql, request.getTableName());
+
+ dataSource.close();
+ return entity;
+ }
+
+ @SneakyThrows
+ private List queryColumns(TableRequest request) {
+ HikariDataSource dataSource = DbUtil.buildFromTableRequest(request);
+ Db db = new Db(dataSource);
+
+ List query = db.query(COLUMN_SQL_TEMPLATE, request.getTableName());
+
+ dataSource.close();
+ return query;
+ }
+
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/utils/CodeGenUtil.java b/demo-codegen/src/main/java/com/xkcoding/codegen/utils/CodeGenUtil.java
new file mode 100644
index 0000000..ace9a53
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/utils/CodeGenUtil.java
@@ -0,0 +1,264 @@
+package com.xkcoding.codegen.utils;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.db.Entity;
+import cn.hutool.setting.dialect.Props;
+import com.google.common.collect.Lists;
+import com.xkcoding.codegen.constants.GenConstants;
+import com.xkcoding.codegen.entity.ColumnEntity;
+import com.xkcoding.codegen.entity.GenConfig;
+import com.xkcoding.codegen.entity.TableEntity;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.text.WordUtils;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ *
+ * 代码生成器 工具类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 09:27
+ */
+@Slf4j
+@UtilityClass
+public class CodeGenUtil {
+
+ private final String ENTITY_JAVA_VM = "Entity.java.vm";
+ private final String MAPPER_JAVA_VM = "Mapper.java.vm";
+ private final String SERVICE_JAVA_VM = "Service.java.vm";
+ private final String SERVICE_IMPL_JAVA_VM = "ServiceImpl.java.vm";
+ private final String CONTROLLER_JAVA_VM = "Controller.java.vm";
+ private final String MAPPER_XML_VM = "Mapper.xml.vm";
+ private final String API_JS_VM = "api.js.vm";
+
+ private List getTemplates() {
+ List templates = new ArrayList<>();
+ templates.add("template/Entity.java.vm");
+ templates.add("template/Mapper.java.vm");
+ templates.add("template/Mapper.xml.vm");
+ templates.add("template/Service.java.vm");
+ templates.add("template/ServiceImpl.java.vm");
+ templates.add("template/Controller.java.vm");
+
+ templates.add("template/api.js.vm");
+ return templates;
+ }
+
+ /**
+ * 生成代码
+ */
+ public void generatorCode(GenConfig genConfig, Entity table, List columns, ZipOutputStream zip) {
+ //配置信息
+ Props propsDB2Java = getConfig("generator.properties");
+ Props propsDB2Jdbc = getConfig("jdbc_type.properties");
+
+ boolean hasBigDecimal = false;
+ //表信息
+ TableEntity tableEntity = new TableEntity();
+ tableEntity.setTableName(table.getStr("tableName"));
+
+ if (StrUtil.isNotBlank(genConfig.getComments())) {
+ tableEntity.setComments(genConfig.getComments());
+ } else {
+ tableEntity.setComments(table.getStr("tableComment"));
+ }
+
+ String tablePrefix;
+ if (StrUtil.isNotBlank(genConfig.getTablePrefix())) {
+ tablePrefix = genConfig.getTablePrefix();
+ } else {
+ tablePrefix = propsDB2Java.getStr("tablePrefix");
+ }
+
+ //表名转换成Java类名
+ String className = tableToJava(tableEntity.getTableName(), tablePrefix);
+ tableEntity.setCaseClassName(className);
+ tableEntity.setLowerClassName(StrUtil.lowerFirst(className));
+
+ //列信息
+ List columnList = Lists.newArrayList();
+ for (Entity column : columns) {
+ ColumnEntity columnEntity = new ColumnEntity();
+ columnEntity.setColumnName(column.getStr("columnName"));
+ columnEntity.setDataType(column.getStr("dataType"));
+ columnEntity.setComments(column.getStr("columnComment"));
+ columnEntity.setExtra(column.getStr("extra"));
+
+ //列名转换成Java属性名
+ String attrName = columnToJava(columnEntity.getColumnName());
+ columnEntity.setCaseAttrName(attrName);
+ columnEntity.setLowerAttrName(StrUtil.lowerFirst(attrName));
+
+ //列的数据类型,转换成Java类型
+ String attrType = propsDB2Java.getStr(columnEntity.getDataType(), "unknownType");
+ columnEntity.setAttrType(attrType);
+ String jdbcType = propsDB2Jdbc.getStr(columnEntity.getDataType(), "unknownType");
+ columnEntity.setJdbcType(jdbcType);
+ if (!hasBigDecimal && "BigDecimal".equals(attrType)) {
+ hasBigDecimal = true;
+ }
+ //是否主键
+ if ("PRI".equalsIgnoreCase(column.getStr("columnKey")) && tableEntity.getPk() == null) {
+ tableEntity.setPk(columnEntity);
+ }
+
+ columnList.add(columnEntity);
+ }
+ tableEntity.setColumns(columnList);
+
+ //没主键,则第一个字段为主键
+ if (tableEntity.getPk() == null) {
+ tableEntity.setPk(tableEntity.getColumns().get(0));
+ }
+
+ //设置velocity资源加载器
+ Properties prop = new Properties();
+ prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+ Velocity.init(prop);
+ //封装模板数据
+ Map map = new HashMap<>(16);
+ map.put("tableName", tableEntity.getTableName());
+ map.put("pk", tableEntity.getPk());
+ map.put("className", tableEntity.getCaseClassName());
+ map.put("classname", tableEntity.getLowerClassName());
+ map.put("pathName", tableEntity.getLowerClassName().toLowerCase());
+ map.put("columns", tableEntity.getColumns());
+ map.put("hasBigDecimal", hasBigDecimal);
+ map.put("datetime", DateUtil.now());
+ map.put("year", DateUtil.year(new Date()));
+
+ if (StrUtil.isNotBlank(genConfig.getComments())) {
+ map.put("comments", genConfig.getComments());
+ } else {
+ map.put("comments", tableEntity.getComments());
+ }
+
+ if (StrUtil.isNotBlank(genConfig.getAuthor())) {
+ map.put("author", genConfig.getAuthor());
+ } else {
+ map.put("author", propsDB2Java.getStr("author"));
+ }
+
+ if (StrUtil.isNotBlank(genConfig.getModuleName())) {
+ map.put("moduleName", genConfig.getModuleName());
+ } else {
+ map.put("moduleName", propsDB2Java.getStr("moduleName"));
+ }
+
+ if (StrUtil.isNotBlank(genConfig.getPackageName())) {
+ map.put("package", genConfig.getPackageName());
+ map.put("mainPath", genConfig.getPackageName());
+ } else {
+ map.put("package", propsDB2Java.getStr("package"));
+ map.put("mainPath", propsDB2Java.getStr("mainPath"));
+ }
+ VelocityContext context = new VelocityContext(map);
+
+ //获取模板列表
+ List templates = getTemplates();
+ for (String template : templates) {
+ //渲染模板
+ StringWriter sw = new StringWriter();
+ Template tpl = Velocity.getTemplate(template, CharsetUtil.UTF_8);
+ tpl.merge(context, sw);
+
+ try {
+ //添加到zip
+ zip.putNextEntry(new ZipEntry(Objects.requireNonNull(getFileName(template, tableEntity.getCaseClassName(), map.get("package").toString(), map.get("moduleName").toString()))));
+ IoUtil.write(zip, StandardCharsets.UTF_8, false, sw.toString());
+ IoUtil.close(sw);
+ zip.closeEntry();
+ } catch (IOException e) {
+ throw new RuntimeException("渲染模板失败,表名:" + tableEntity.getTableName(), e);
+ }
+ }
+ }
+
+
+ /**
+ * 列名转换成Java属性名
+ */
+ private String columnToJava(String columnName) {
+ return WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", "");
+ }
+
+ /**
+ * 表名转换成Java类名
+ */
+ private String tableToJava(String tableName, String tablePrefix) {
+ if (StrUtil.isNotBlank(tablePrefix)) {
+ tableName = tableName.replaceFirst(tablePrefix, "");
+ }
+ return columnToJava(tableName);
+ }
+
+ /**
+ * 获取配置信息
+ */
+ private Props getConfig(String fileName) {
+ Props props = new Props(fileName);
+ props.autoLoad(true);
+ return props;
+ }
+
+ /**
+ * 获取文件名
+ */
+ private String getFileName(String template, String className, String packageName, String moduleName) {
+ // 包路径
+ String packagePath = GenConstants.SIGNATURE + File.separator + "src" + File.separator + "main" + File.separator + "java" + File.separator;
+ // 资源路径
+ String resourcePath = GenConstants.SIGNATURE + File.separator + "src" + File.separator + "main" + File.separator + "resources" + File.separator;
+ // api路径
+ String apiPath = GenConstants.SIGNATURE + File.separator + "api" + File.separator;
+
+ if (StrUtil.isNotBlank(packageName)) {
+ packagePath += packageName.replace(".", File.separator) + File.separator + moduleName + File.separator;
+ }
+
+ if (template.contains(ENTITY_JAVA_VM)) {
+ return packagePath + "entity" + File.separator + className + ".java";
+ }
+
+ if (template.contains(MAPPER_JAVA_VM)) {
+ return packagePath + "mapper" + File.separator + className + "Mapper.java";
+ }
+
+ if (template.contains(SERVICE_JAVA_VM)) {
+ return packagePath + "service" + File.separator + className + "Service.java";
+ }
+
+ if (template.contains(SERVICE_IMPL_JAVA_VM)) {
+ return packagePath + "service" + File.separator + "impl" + File.separator + className + "ServiceImpl.java";
+ }
+
+ if (template.contains(CONTROLLER_JAVA_VM)) {
+ return packagePath + "controller" + File.separator + className + "Controller.java";
+ }
+
+ if (template.contains(MAPPER_XML_VM)) {
+ return resourcePath + "mapper" + File.separator + className + "Mapper.xml";
+ }
+
+ if (template.contains(API_JS_VM)) {
+ return apiPath + className.toLowerCase() + ".js";
+ }
+
+ return null;
+ }
+}
diff --git a/demo-codegen/src/main/java/com/xkcoding/codegen/utils/DbUtil.java b/demo-codegen/src/main/java/com/xkcoding/codegen/utils/DbUtil.java
new file mode 100644
index 0000000..17503cc
--- /dev/null
+++ b/demo-codegen/src/main/java/com/xkcoding/codegen/utils/DbUtil.java
@@ -0,0 +1,27 @@
+package com.xkcoding.codegen.utils;
+
+import com.xkcoding.codegen.entity.TableRequest;
+import com.zaxxer.hikari.HikariDataSource;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ *
+ * 数据库工具类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:26
+ */
+@Slf4j
+@UtilityClass
+public class DbUtil {
+ public HikariDataSource buildFromTableRequest(TableRequest request) {
+ HikariDataSource dataSource = new HikariDataSource();
+ dataSource.setJdbcUrl(request.getPrepend() + request.getUrl());
+ dataSource.setUsername(request.getUsername());
+ dataSource.setPassword(request.getPassword());
+ return dataSource;
+ }
+
+}
diff --git a/spring-boot-demo-codegen/src/main/resources/application.yml b/demo-codegen/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/application.yml
rename to demo-codegen/src/main/resources/application.yml
diff --git a/spring-boot-demo-codegen/src/main/resources/generator.properties b/demo-codegen/src/main/resources/generator.properties
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/generator.properties
rename to demo-codegen/src/main/resources/generator.properties
diff --git a/spring-boot-demo-codegen/src/main/resources/jdbc_type.properties b/demo-codegen/src/main/resources/jdbc_type.properties
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/jdbc_type.properties
rename to demo-codegen/src/main/resources/jdbc_type.properties
diff --git a/demo-codegen/src/main/resources/logback-spring.xml b/demo-codegen/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..dcd48fe
--- /dev/null
+++ b/demo-codegen/src/main/resources/logback-spring.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+ INFO
+
+
+ ${CONSOLE_LOG_PATTERN}
+ UTF-8
+
+
+
+
+
+
+
+ ERROR
+
+ DENY
+
+ ACCEPT
+
+
+
+
+
+
+ logs/demo-logback/info.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+
+
+ 2MB
+
+
+
+
+
+
+ ${FILE_LOG_PATTERN}
+ UTF-8
+
+
+
+
+
+
+ Error
+
+
+
+
+
+
+ logs/demo-logback/error.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+ 2MB
+
+
+
+ ${FILE_ERROR_PATTERN}
+ UTF-8
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-codegen/src/main/resources/static/index.html b/demo-codegen/src/main/resources/static/index.html
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/static/index.html
rename to demo-codegen/src/main/resources/static/index.html
diff --git a/spring-boot-demo-codegen/src/main/resources/static/libs/axios/axios.min.js b/demo-codegen/src/main/resources/static/libs/axios/axios.min.js
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/static/libs/axios/axios.min.js
rename to demo-codegen/src/main/resources/static/libs/axios/axios.min.js
diff --git a/demo-codegen/src/main/resources/static/libs/datejs/date-zh-CN.js b/demo-codegen/src/main/resources/static/libs/datejs/date-zh-CN.js
new file mode 100644
index 0000000..1fa0daa
--- /dev/null
+++ b/demo-codegen/src/main/resources/static/libs/datejs/date-zh-CN.js
@@ -0,0 +1,145 @@
+/**
+ * @version: 1.0 Alpha-1
+ * @author Coolite Inc. http://www.coolite.com/
+ * @date: 2008-05-13
+ * @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved.
+ * @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
+ * @website: http://www.datejs.com/
+ */
+Date.CultureInfo={name:"zh-CN",englishName:"Chinese (People's Republic of China)",nativeName:"中文(中华人民共和国)",dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],abbreviatedDayNames:["日","一","二","三","四","五","六"],shortestDayNames:["日","一","二","三","四","五","六"],firstLetterDayNames:["日","一","二","三","四","五","六"],monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],abbreviatedMonthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],amDesignator:"上午",pmDesignator:"下午",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"ymd",formatPatterns:{shortDate:"yyyy/M/d",longDate:"yyyy'年'M'月'd'日'",shortTime:"H:mm",longTime:"H:mm:ss",fullDateTime:"yyyy'年'M'月'd'日' H:mm:ss",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"M'月'd'日'",yearMonth:"yyyy'年'M'月'"},regexPatterns:{jan:/^一月/i,feb:/^二月/i,mar:/^三月/i,apr:/^四月/i,may:/^五月/i,jun:/^六月/i,jul:/^七月/i,aug:/^八月/i,sep:/^九月/i,oct:/^十月/i,nov:/^十一月/i,dec:/^十二月/i,sun:/^星期日/i,mon:/^星期一/i,tue:/^星期二/i,wed:/^星期三/i,thu:/^星期四/i,fri:/^星期五/i,sat:/^星期六/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]};
+(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,p=function(s,l){if(!l){l=2;}
+return("000"+s).slice(l*-1);};$P.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};$P.setTimeToNow=function(){var n=new Date();this.setHours(n.getHours());this.setMinutes(n.getMinutes());this.setSeconds(n.getSeconds());this.setMilliseconds(n.getMilliseconds());return this;};$D.today=function(){return new Date().clearTime();};$D.compare=function(date1,date2){if(isNaN(date1)||isNaN(date2)){throw new Error(date1+" - "+date2);}else if(date1 instanceof Date&&date2 instanceof Date){return(date1date2)?1:0;}else{throw new TypeError(date1+" - "+date2);}};$D.equals=function(date1,date2){return(date1.compareTo(date2)===0);};$D.getDayNumberFromName=function(name){var n=$C.dayNames,m=$C.abbreviatedDayNames,o=$C.shortestDayNames,s=name.toLowerCase();for(var i=0;i=start.getTime()&&this.getTime()<=end.getTime();};$P.isAfter=function(date){return this.compareTo(date||new Date())===1;};$P.isBefore=function(date){return(this.compareTo(date||new Date())===-1);};$P.isToday=function(){return this.isSameDay(new Date());};$P.isSameDay=function(date){return this.clone().clearTime().equals(date.clone().clearTime());};$P.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};$P.addSeconds=function(value){return this.addMilliseconds(value*1000);};$P.addMinutes=function(value){return this.addMilliseconds(value*60000);};$P.addHours=function(value){return this.addMilliseconds(value*3600000);};$P.addDays=function(value){this.setDate(this.getDate()+value);return this;};$P.addWeeks=function(value){return this.addDays(value*7);};$P.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,$D.getDaysInMonth(this.getFullYear(),this.getMonth())));return this;};$P.addYears=function(value){return this.addMonths(value*12);};$P.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
+var x=config;if(x.milliseconds){this.addMilliseconds(x.milliseconds);}
+if(x.seconds){this.addSeconds(x.seconds);}
+if(x.minutes){this.addMinutes(x.minutes);}
+if(x.hours){this.addHours(x.hours);}
+if(x.weeks){this.addWeeks(x.weeks);}
+if(x.months){this.addMonths(x.months);}
+if(x.years){this.addYears(x.years);}
+if(x.days){this.addDays(x.days);}
+return this;};var $y,$m,$d;$P.getWeek=function(){var a,b,c,d,e,f,g,n,s,w;$y=(!$y)?this.getFullYear():$y;$m=(!$m)?this.getMonth()+1:$m;$d=(!$d)?this.getDate():$d;if($m<=2){a=$y-1;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=0;f=$d-1+(31*($m-1));}else{a=$y;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=s+1;f=$d+((153*($m-3)+2)/5)+58+s;}
+g=(a+b)%7;d=(f+g-e)%7;n=(f+3-d)|0;if(n<0){w=53-((g-s)/5|0);}else if(n>364+s){w=1;}else{w=(n/7|0)+1;}
+$y=$m=$d=null;return w;};$P.getISOWeek=function(){$y=this.getUTCFullYear();$m=this.getUTCMonth()+1;$d=this.getUTCDate();return p(this.getWeek());};$P.setWeek=function(n){return this.moveToDayOfWeek(1).addWeeks(n-this.getWeek());};$D._validate=function(n,min,max,name){if(typeof n=="undefined"){return false;}else if(typeof n!="number"){throw new TypeError(n+" is not a Number.");}else if(nmax){throw new RangeError(n+" is not a valid value for "+name+".");}
+return true;};$D.validateMillisecond=function(value){return $D._validate(value,0,999,"millisecond");};$D.validateSecond=function(value){return $D._validate(value,0,59,"second");};$D.validateMinute=function(value){return $D._validate(value,0,59,"minute");};$D.validateHour=function(value){return $D._validate(value,0,23,"hour");};$D.validateDay=function(value,year,month){return $D._validate(value,1,$D.getDaysInMonth(year,month),"day");};$D.validateMonth=function(value){return $D._validate(value,0,11,"month");};$D.validateYear=function(value){return $D._validate(value,0,9999,"year");};$P.set=function(config){if($D.validateMillisecond(config.millisecond)){this.addMilliseconds(config.millisecond-this.getMilliseconds());}
+if($D.validateSecond(config.second)){this.addSeconds(config.second-this.getSeconds());}
+if($D.validateMinute(config.minute)){this.addMinutes(config.minute-this.getMinutes());}
+if($D.validateHour(config.hour)){this.addHours(config.hour-this.getHours());}
+if($D.validateMonth(config.month)){this.addMonths(config.month-this.getMonth());}
+if($D.validateYear(config.year)){this.addYears(config.year-this.getFullYear());}
+if($D.validateDay(config.day,this.getFullYear(),this.getMonth())){this.addDays(config.day-this.getDate());}
+if(config.timezone){this.setTimezone(config.timezone);}
+if(config.timezoneOffset){this.setTimezoneOffset(config.timezoneOffset);}
+if(config.week&&$D._validate(config.week,0,53,"week")){this.setWeek(config.week);}
+return this;};$P.moveToFirstDayOfMonth=function(){return this.set({day:1});};$P.moveToLastDayOfMonth=function(){return this.set({day:$D.getDaysInMonth(this.getFullYear(),this.getMonth())});};$P.moveToNthOccurrence=function(dayOfWeek,occurrence){var shift=0;if(occurrence>0){shift=occurrence-1;}
+else if(occurrence===-1){this.moveToLastDayOfMonth();if(this.getDay()!==dayOfWeek){this.moveToDayOfWeek(dayOfWeek,-1);}
+return this;}
+return this.moveToFirstDayOfMonth().addDays(-1).moveToDayOfWeek(dayOfWeek,+1).addWeeks(shift);};$P.moveToDayOfWeek=function(dayOfWeek,orient){var diff=(dayOfWeek-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};$P.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};$P.getOrdinalNumber=function(){return Math.ceil((this.clone().clearTime()-new Date(this.getFullYear(),0,1))/86400000)+1;};$P.getTimezone=function(){return $D.getTimezoneAbbreviation(this.getUTCOffset());};$P.setTimezoneOffset=function(offset){var here=this.getTimezoneOffset(),there=Number(offset)*-6/10;return this.addMinutes(there-here);};$P.setTimezone=function(offset){return this.setTimezoneOffset($D.getTimezoneOffset(offset));};$P.hasDaylightSavingTime=function(){return(Date.today().set({month:0,day:1}).getTimezoneOffset()!==Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.isDaylightSavingTime=function(){return(this.hasDaylightSavingTime()&&new Date().getTimezoneOffset()===Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r.charAt(0)+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};$P.getElapsed=function(date){return(date||new Date())-this;};if(!$P.toISOString){$P.toISOString=function(){function f(n){return n<10?'0'+n:n;}
+return'"'+this.getUTCFullYear()+'-'+
+f(this.getUTCMonth()+1)+'-'+
+f(this.getUTCDate())+'T'+
+f(this.getUTCHours())+':'+
+f(this.getUTCMinutes())+':'+
+f(this.getUTCSeconds())+'Z"';};}
+$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}}
+var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");}
+x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}());
+(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());}
+return false;};$P.at=function(time){return(typeof time==="string")?$D.parse(this.toString("d")+" "+time):this.set(time);};$N.fromNow=$N.after=function(date){var c={};c[this._dateElement]=this;return((!date)?new Date():date.clone()).add(c);};$N.ago=$N.before=function(date){var c={};c[this._dateElement]=this*-1;return((!date)?new Date():date.clone()).add(c);};var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),pxf=("Milliseconds Seconds Minutes Hours Date Week Month FullYear").split(/\s/),nth=("final first second third fourth fifth").split(/\s/),de;$P.toObject=function(){var o={};for(var i=0;itemp){throw new RangeError($D.getDayName(n)+" does not occur "+ntemp+" times in the month of "+$D.getMonthName(temp.getMonth())+" "+temp.getFullYear()+".");}
+return this;}
+return this.moveToDayOfWeek(n,this._orient);};};var sdf=function(n){return function(){var t=$D.today(),shift=n-t.getDay();if(n===0&&$C.firstDayOfWeek===1&&t.getDay()!==0){shift=shift+7;}
+return t.addDays(shift);};};for(var i=0;i-1;m--){v=px[m].toLowerCase();if(o1[v]!=o2[v]){return false;}
+if(k==v){break;}}
+return true;}
+if(j.substring(j.length-1)!="s"){j+="s";}
+return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
+if(!last&&q[1].length===0){last=true;}
+if(!last){var qx=[];for(var j=0;j0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
+if(rx[1].length1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
+if(args){for(var i=0,px=args.shift();i2)?n:(n+(((n+2000)<$C.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];for(var i=0;i$D.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
+var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
+return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
+for(var i=0;i
+ * ${comments}
+ *
+ *
+ * @author ${author}
+ * @date Created in ${datetime}
+ */
+@Slf4j
+@RestController
+@RequestMapping("/${pathName}")
+@Api(description = "${className}Controller", tags = {"${comments}"})
+public class ${className}Controller {
+ @Autowired
+ private ${className}Service ${classname}Service;
+
+ /**
+ * 分页查询${comments}
+ * @param page 分页对象
+ * @param ${classname} ${comments}
+ * @return R
+ */
+ @GetMapping("")
+ @ApiOperation(value = "分页查询${comments}", notes = "分页查询${comments}")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "page", value = "分页参数", required = true),
+ @ApiImplicitParam(name = "${classname}", value = "查询条件", required = true)
+ })
+ public R list${className}(Page page, ${className} ${classname}) {
+ return R.success(${classname}Service.page(page,Wrappers.query(${classname})));
+ }
+
+
+ /**
+ * 通过id查询${comments}
+ * @param ${pk.lowerAttrName} id
+ * @return R
+ */
+ @GetMapping("/{${pk.lowerAttrName}}")
+ @ApiOperation(value = "通过id查询${comments}", notes = "通过id查询${comments}")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "${pk.lowerAttrName}", value = "主键id", required = true)
+ })
+ public R get${className}(@PathVariable("${pk.lowerAttrName}") ${pk.attrType} ${pk.lowerAttrName}){
+ return R.success(${classname}Service.getById(${pk.lowerAttrName}));
+ }
+
+ /**
+ * 新增${comments}
+ * @param ${classname} ${comments}
+ * @return R
+ */
+ @PostMapping
+ @ApiOperation(value = "新增${comments}", notes = "新增${comments}")
+ public R save${className}(@RequestBody ${className} ${classname}){
+ return R.success(${classname}Service.save(${classname}));
+ }
+
+ /**
+ * 修改${comments}
+ * @param ${pk.lowerAttrName} id
+ * @param ${classname} ${comments}
+ * @return R
+ */
+ @PutMapping("/{${pk.lowerAttrName}}")
+ @ApiOperation(value = "修改${comments}", notes = "修改${comments}")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "${pk.lowerAttrName}", value = "主键id", required = true)
+ })
+ public R update${className}(@PathVariable ${pk.attrType} ${pk.lowerAttrName}, @RequestBody ${className} ${classname}){
+ return R.success(${classname}Service.updateById(${classname}));
+ }
+
+ /**
+ * 通过id删除${comments}
+ * @param ${pk.lowerAttrName} id
+ * @return R
+ */
+ @DeleteMapping("/{${pk.lowerAttrName}}")
+ @ApiOperation(value = "删除${comments}", notes = "删除${comments}")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "${pk.lowerAttrName}", value = "主键id", required = true)
+ })
+ public R delete${className}(@PathVariable ${pk.attrType} ${pk.lowerAttrName}){
+ return R.success(${classname}Service.removeById(${pk.lowerAttrName}));
+ }
+
+}
diff --git a/demo-codegen/src/main/resources/template/Entity.java.vm b/demo-codegen/src/main/resources/template/Entity.java.vm
new file mode 100755
index 0000000..fd2c004
--- /dev/null
+++ b/demo-codegen/src/main/resources/template/Entity.java.vm
@@ -0,0 +1,42 @@
+package ${package}.${moduleName}.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+#if(${hasBigDecimal})
+import java.math.BigDecimal;
+#end
+import java.time.LocalDateTime;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.NoArgsConstructor;
+/**
+ *
+ * ${comments}
+ *
+ *
+ * @author ${author}
+ * @date Created in ${datetime}
+ */
+@Data
+@NoArgsConstructor
+@TableName("${tableName}")
+@ApiModel(description = "${comments}")
+@EqualsAndHashCode(callSuper = true)
+public class ${className} extends Model<${className}> {
+ private static final long serialVersionUID = 1L;
+
+ #foreach ($column in $columns)
+ /**
+ * $column.comments
+ */
+ #if($column.columnName == $pk.columnName)
+ @TableId
+ #end
+ @ApiModelProperty(value = "$column.comments")
+ private $column.attrType $column.lowerAttrName;
+ #end
+
+}
diff --git a/demo-codegen/src/main/resources/template/Mapper.java.vm b/demo-codegen/src/main/resources/template/Mapper.java.vm
new file mode 100755
index 0000000..f43178f
--- /dev/null
+++ b/demo-codegen/src/main/resources/template/Mapper.java.vm
@@ -0,0 +1,18 @@
+package ${package}.${moduleName}.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springframework.stereotype.Component;
+import ${package}.${moduleName}.entity.${className};
+
+/**
+ *
+ * ${comments}
+ *
+ *
+ * @author ${author}
+ * @date Created in ${datetime}
+ */
+@Component
+public interface ${className}Mapper extends BaseMapper<${className}> {
+
+}
diff --git a/spring-boot-demo-codegen/src/main/resources/template/Mapper.xml.vm b/demo-codegen/src/main/resources/template/Mapper.xml.vm
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/template/Mapper.xml.vm
rename to demo-codegen/src/main/resources/template/Mapper.xml.vm
diff --git a/demo-codegen/src/main/resources/template/Service.java.vm b/demo-codegen/src/main/resources/template/Service.java.vm
new file mode 100755
index 0000000..c84c7ec
--- /dev/null
+++ b/demo-codegen/src/main/resources/template/Service.java.vm
@@ -0,0 +1,16 @@
+package ${package}.${moduleName}.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import ${package}.${moduleName}.entity.${className};
+
+/**
+ *
+ * ${comments}
+ *
+ *
+ * @author ${author}
+ * @date Created in ${datetime}
+ */
+public interface ${className}Service extends IService<${className}> {
+
+}
diff --git a/demo-codegen/src/main/resources/template/ServiceImpl.java.vm b/demo-codegen/src/main/resources/template/ServiceImpl.java.vm
new file mode 100755
index 0000000..01a881d
--- /dev/null
+++ b/demo-codegen/src/main/resources/template/ServiceImpl.java.vm
@@ -0,0 +1,21 @@
+package ${package}.${moduleName}.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import ${package}.${moduleName}.entity.${className};
+import ${package}.${moduleName}.mapper.${className}Mapper;
+import ${package}.${moduleName}.service.${className}Service;
+import org.springframework.stereotype.Service;
+import lombok.extern.slf4j.Slf4j;
+/**
+ *
+ * ${comments}
+ *
+ *
+ * @author ${author}
+ * @date Created in ${datetime}
+ */
+@Service
+@Slf4j
+public class ${className}ServiceImpl extends ServiceImpl<${className}Mapper, ${className}> implements ${className}Service {
+
+}
diff --git a/spring-boot-demo-codegen/src/main/resources/template/api.js.vm b/demo-codegen/src/main/resources/template/api.js.vm
similarity index 100%
rename from spring-boot-demo-codegen/src/main/resources/template/api.js.vm
rename to demo-codegen/src/main/resources/template/api.js.vm
diff --git a/demo-codegen/src/test/java/com/xkcoding/codegen/CodeGenServiceTest.java b/demo-codegen/src/test/java/com/xkcoding/codegen/CodeGenServiceTest.java
new file mode 100644
index 0000000..e11cb5d
--- /dev/null
+++ b/demo-codegen/src/test/java/com/xkcoding/codegen/CodeGenServiceTest.java
@@ -0,0 +1,74 @@
+package com.xkcoding.codegen;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.db.Entity;
+import com.xkcoding.codegen.common.PageResult;
+import com.xkcoding.codegen.entity.GenConfig;
+import com.xkcoding.codegen.entity.TableRequest;
+import com.xkcoding.codegen.service.CodeGenService;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+
+/**
+ *
+ * 代码生成service测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-03-22 10:34
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@Slf4j
+public class CodeGenServiceTest {
+ @Autowired
+ private CodeGenService codeGenService;
+
+ @Test
+ public void testTablePage() {
+ TableRequest request = new TableRequest();
+ request.setCurrentPage(1);
+ request.setPageSize(10);
+ request.setPrepend("jdbc:mysql://");
+ request.setUrl("127.0.0.1:3306/spring-boot-demo");
+ request.setUsername("root");
+ request.setPassword("root");
+ request.setTableName("sec_");
+ PageResult pageResult = codeGenService.listTables(request);
+ log.info("【pageResult】= {}", pageResult);
+ }
+
+ @Test
+ @SneakyThrows
+ public void testGeneratorCode() {
+ GenConfig config = new GenConfig();
+
+ TableRequest request = new TableRequest();
+ request.setPrepend("jdbc:mysql://");
+ request.setUrl("127.0.0.1:3306/spring-boot-demo");
+ request.setUsername("root");
+ request.setPassword("root");
+ request.setTableName("shiro_user");
+ config.setRequest(request);
+
+ config.setModuleName("shiro");
+ config.setAuthor("Yangkai.Shen");
+ config.setComments("用户角色信息");
+ config.setPackageName("com.xkcoding");
+ config.setTablePrefix("shiro_");
+
+ byte[] zip = codeGenService.generatorCode(config);
+ OutputStream outputStream = new FileOutputStream(new File("/Users/yangkai.shen/Desktop/" + request.getTableName() + ".zip"));
+ IoUtil.write(outputStream, true, zip);
+ }
+
+}
diff --git a/spring-boot-demo-codegen/src/test/java/com/xkcoding/codegen/SpringBootDemoCodegenApplicationTests.java b/demo-codegen/src/test/java/com/xkcoding/codegen/SpringBootDemoCodegenApplicationTests.java
similarity index 100%
rename from spring-boot-demo-codegen/src/test/java/com/xkcoding/codegen/SpringBootDemoCodegenApplicationTests.java
rename to demo-codegen/src/test/java/com/xkcoding/codegen/SpringBootDemoCodegenApplicationTests.java
diff --git a/spring-boot-demo-docker/.gitignore b/demo-docker/.gitignore
similarity index 100%
rename from spring-boot-demo-docker/.gitignore
rename to demo-docker/.gitignore
diff --git a/spring-boot-demo-docker/Dockerfile b/demo-docker/Dockerfile
similarity index 100%
rename from spring-boot-demo-docker/Dockerfile
rename to demo-docker/Dockerfile
diff --git a/spring-boot-demo-docker/README.md b/demo-docker/README.md
similarity index 100%
rename from spring-boot-demo-docker/README.md
rename to demo-docker/README.md
diff --git a/demo-docker/pom.xml b/demo-docker/pom.xml
new file mode 100644
index 0000000..c489b51
--- /dev/null
+++ b/demo-docker/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+ demo-docker
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-docker
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 1.4.9
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ demo-docker
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ com.spotify
+ dockerfile-maven-plugin
+ ${dockerfile-version}
+
+ ${project.build.finalName}
+ ${project.version}
+
+ target/${project.build.finalName}.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo-docker/src/main/java/com/xkcoding/docker/SpringBootDemoDockerApplication.java b/demo-docker/src/main/java/com/xkcoding/docker/SpringBootDemoDockerApplication.java
new file mode 100644
index 0000000..c1a4004
--- /dev/null
+++ b/demo-docker/src/main/java/com/xkcoding/docker/SpringBootDemoDockerApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.docker;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-29 14:59
+ */
+@SpringBootApplication
+public class SpringBootDemoDockerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDockerApplication.class, args);
+ }
+}
diff --git a/demo-docker/src/main/java/com/xkcoding/docker/controller/HelloController.java b/demo-docker/src/main/java/com/xkcoding/docker/controller/HelloController.java
new file mode 100644
index 0000000..a8b0d39
--- /dev/null
+++ b/demo-docker/src/main/java/com/xkcoding/docker/controller/HelloController.java
@@ -0,0 +1,22 @@
+package com.xkcoding.docker.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * Hello Controller
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-29 14:58
+ */
+@RestController
+@RequestMapping
+public class HelloController {
+ @GetMapping
+ public String hello() {
+ return "Hello,From Docker!";
+ }
+}
diff --git a/spring-boot-demo-docker/src/main/resources/application.yml b/demo-docker/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-docker/src/main/resources/application.yml
rename to demo-docker/src/main/resources/application.yml
diff --git a/spring-boot-demo-docker/src/test/java/com/xkcoding/docker/SpringBootDemoDockerApplicationTests.java b/demo-docker/src/test/java/com/xkcoding/docker/SpringBootDemoDockerApplicationTests.java
similarity index 100%
rename from spring-boot-demo-docker/src/test/java/com/xkcoding/docker/SpringBootDemoDockerApplicationTests.java
rename to demo-docker/src/test/java/com/xkcoding/docker/SpringBootDemoDockerApplicationTests.java
diff --git a/spring-boot-demo-dubbo/.gitignore b/demo-dubbo/.gitignore
similarity index 100%
rename from spring-boot-demo-dubbo/.gitignore
rename to demo-dubbo/.gitignore
diff --git a/spring-boot-demo-dubbo/README.md b/demo-dubbo/README.md
similarity index 100%
rename from spring-boot-demo-dubbo/README.md
rename to demo-dubbo/README.md
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-common/.gitignore b/demo-dubbo/dubbo-common/.gitignore
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-common/.gitignore
rename to demo-dubbo/dubbo-common/.gitignore
diff --git a/demo-dubbo/dubbo-common/README.md b/demo-dubbo/dubbo-common/README.md
new file mode 100644
index 0000000..a44f8f9
--- /dev/null
+++ b/demo-dubbo/dubbo-common/README.md
@@ -0,0 +1,55 @@
+# spring-boot-demo-dubbo-common
+
+> 此 module 主要是用于公共部分,主要存放工具类,实体,以及服务提供方/调用方的接口定义
+
+## pom.xml
+
+```xml
+
+
+
+ spring-boot-demo-dubbo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-boot-demo-dubbo-common
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+ spring-boot-demo-dubbo-common
+
+
+
+```
+
+## HelloService.java
+
+```java
+/**
+ *
+ * Hello服务接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:56
+ */
+public interface HelloService {
+ /**
+ * 问好
+ *
+ * @param name 姓名
+ * @return 问好
+ */
+ String sayHello(String name);
+}
+```
+
diff --git a/demo-dubbo/dubbo-common/pom.xml b/demo-dubbo/dubbo-common/pom.xml
new file mode 100644
index 0000000..c448fdf
--- /dev/null
+++ b/demo-dubbo/dubbo-common/pom.xml
@@ -0,0 +1,24 @@
+
+
+
+ demo-dubbo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ dubbo-common
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+ dubbo-common
+
+
+
diff --git a/demo-dubbo/dubbo-common/src/main/java/com/xkcoding/dubbo/common/service/HelloService.java b/demo-dubbo/dubbo-common/src/main/java/com/xkcoding/dubbo/common/service/HelloService.java
new file mode 100644
index 0000000..9b52d75
--- /dev/null
+++ b/demo-dubbo/dubbo-common/src/main/java/com/xkcoding/dubbo/common/service/HelloService.java
@@ -0,0 +1,19 @@
+package com.xkcoding.dubbo.common.service;
+
+/**
+ *
+ * Hello服务接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:56
+ */
+public interface HelloService {
+ /**
+ * 问好
+ *
+ * @param name 姓名
+ * @return 问好
+ */
+ String sayHello(String name);
+}
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-consumer/.gitignore b/demo-dubbo/dubbo-consumer/.gitignore
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-consumer/.gitignore
rename to demo-dubbo/dubbo-consumer/.gitignore
diff --git a/demo-dubbo/dubbo-consumer/README.md b/demo-dubbo/dubbo-consumer/README.md
new file mode 100644
index 0000000..6ff76e0
--- /dev/null
+++ b/demo-dubbo/dubbo-consumer/README.md
@@ -0,0 +1,130 @@
+# spring-boot-demo-dubbo-consumer
+
+> 此 module 主要是服务调用方的示例
+
+## pom.xml
+
+```xml
+
+
+
+ spring-boot-demo-dubbo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-boot-demo-dubbo-consumer
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.alibaba.spring.boot
+ dubbo-spring-boot-starter
+ ${dubbo.starter.version}
+
+
+
+ ${project.groupId}
+ spring-boot-demo-dubbo-common
+ ${project.version}
+
+
+
+ com.101tec
+ zkclient
+ ${zkclient.version}
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ spring-boot-demo-dubbo-consumer
+
+
+
+```
+
+## application.yml
+
+```yaml
+server:
+ port: 8080
+ servlet:
+ context-path: /demo
+
+spring:
+ dubbo:
+ application:
+ name: spring-boot-demo-dubbo-consumer
+ registry: zookeeper://127.0.0.1:2181
+```
+
+## SpringBootDemoDubboConsumerApplication.java
+
+```java
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:49
+ */
+@SpringBootApplication
+@EnableDubboConfiguration
+public class SpringBootDemoDubboConsumerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDubboConsumerApplication.class, args);
+ }
+}
+```
+
+## HelloController.java
+
+```java
+/**
+ *
+ * Hello服务API
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 17:22
+ */
+@RestController
+@Slf4j
+public class HelloController {
+ @Reference
+ private HelloService helloService;
+
+ @GetMapping("/sayHello")
+ public String sayHello(@RequestParam(defaultValue = "xkcoding") String name) {
+ log.info("i'm ready to call someone......");
+ return helloService.sayHello(name);
+ }
+}
+```
diff --git a/demo-dubbo/dubbo-consumer/pom.xml b/demo-dubbo/dubbo-consumer/pom.xml
new file mode 100644
index 0000000..ed6db99
--- /dev/null
+++ b/demo-dubbo/dubbo-consumer/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ demo-dubbo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ dubbo-consumer
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.alibaba.spring.boot
+ dubbo-spring-boot-starter
+ ${dubbo.starter.version}
+
+
+
+ ${project.groupId}
+ dubbo-common
+ ${project.version}
+
+
+
+ com.101tec
+ zkclient
+ ${zkclient.version}
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ dubbo-consumer
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplication.java b/demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplication.java
new file mode 100644
index 0000000..68a5e61
--- /dev/null
+++ b/demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.dubbo.consumer;
+
+import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:49
+ */
+@SpringBootApplication
+@EnableDubboConfiguration
+public class SpringBootDemoDubboConsumerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDubboConsumerApplication.class, args);
+ }
+}
diff --git a/demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/controller/HelloController.java b/demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/controller/HelloController.java
new file mode 100644
index 0000000..026032e
--- /dev/null
+++ b/demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/controller/HelloController.java
@@ -0,0 +1,29 @@
+package com.xkcoding.dubbo.consumer.controller;
+
+import com.alibaba.dubbo.config.annotation.Reference;
+import com.xkcoding.dubbo.common.service.HelloService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * Hello服务API
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 17:22
+ */
+@RestController
+@Slf4j
+public class HelloController {
+ @Reference
+ private HelloService helloService;
+
+ @GetMapping("/sayHello")
+ public String sayHello(@RequestParam(defaultValue = "xkcoding") String name) {
+ log.info("i'm ready to call someone......");
+ return helloService.sayHello(name);
+ }
+}
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-consumer/src/main/resources/application.yml b/demo-dubbo/dubbo-consumer/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-consumer/src/main/resources/application.yml
rename to demo-dubbo/dubbo-consumer/src/main/resources/application.yml
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-consumer/src/test/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplicationTests.java b/demo-dubbo/dubbo-consumer/src/test/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplicationTests.java
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-consumer/src/test/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplicationTests.java
rename to demo-dubbo/dubbo-consumer/src/test/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplicationTests.java
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-provider/.gitignore b/demo-dubbo/dubbo-provider/.gitignore
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-provider/.gitignore
rename to demo-dubbo/dubbo-provider/.gitignore
diff --git a/demo-dubbo/dubbo-provider/README.md b/demo-dubbo/dubbo-provider/README.md
new file mode 100644
index 0000000..a6b1774
--- /dev/null
+++ b/demo-dubbo/dubbo-provider/README.md
@@ -0,0 +1,135 @@
+# spring-boot-demo-dubbo-provider
+
+> 此 module 主要是服务提供方示例
+
+## pom.xml
+
+```xml
+
+
+
+ spring-boot-demo-dubbo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-boot-demo-dubbo-provider
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.alibaba.spring.boot
+ dubbo-spring-boot-starter
+ ${dubbo.starter.version}
+
+
+
+ ${project.groupId}
+ spring-boot-demo-dubbo-common
+ ${project.version}
+
+
+
+ com.101tec
+ zkclient
+ ${zkclient.version}
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ spring-boot-demo-dubbo-provider
+
+
+
+```
+
+## application.yml
+
+```yaml
+server:
+ port: 9090
+ servlet:
+ context-path: /demo
+
+spring:
+ dubbo:
+ application:
+ name: spring-boot-demo-dubbo-provider
+ registry: zookeeper://localhost:2181
+```
+
+## SpringBootDemoDubboProviderApplication.java
+
+```java
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:49
+ */
+@EnableDubboConfiguration
+@SpringBootApplication
+public class SpringBootDemoDubboProviderApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDubboProviderApplication.class, args);
+ }
+}
+```
+
+## HelloServiceImpl.java
+
+```java
+/**
+ *
+ * Hello服务实现
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:58
+ */
+@Service
+@Component
+@Slf4j
+public class HelloServiceImpl implements HelloService {
+ /**
+ * 问好
+ *
+ * @param name 姓名
+ * @return 问好
+ */
+ @Override
+ public String sayHello(String name) {
+ log.info("someone is calling me......");
+ return "say hello to: " + name;
+ }
+}
+```
+
diff --git a/demo-dubbo/dubbo-provider/pom.xml b/demo-dubbo/dubbo-provider/pom.xml
new file mode 100644
index 0000000..6fdd742
--- /dev/null
+++ b/demo-dubbo/dubbo-provider/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ demo-dubbo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ dubbo-provider
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.alibaba.spring.boot
+ dubbo-spring-boot-starter
+ ${dubbo.starter.version}
+
+
+
+ ${project.groupId}
+ dubbo-common
+ ${project.version}
+
+
+
+ com.101tec
+ zkclient
+ ${zkclient.version}
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ dubbo-provider
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplication.java b/demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplication.java
new file mode 100644
index 0000000..c34b5de
--- /dev/null
+++ b/demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.dubbo.provider;
+
+import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:49
+ */
+@EnableDubboConfiguration
+@SpringBootApplication
+public class SpringBootDemoDubboProviderApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDubboProviderApplication.class, args);
+ }
+}
diff --git a/demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/service/HelloServiceImpl.java b/demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/service/HelloServiceImpl.java
new file mode 100644
index 0000000..9a69537
--- /dev/null
+++ b/demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/service/HelloServiceImpl.java
@@ -0,0 +1,31 @@
+package com.xkcoding.dubbo.provider.service;
+
+import com.alibaba.dubbo.config.annotation.Service;
+import com.xkcoding.dubbo.common.service.HelloService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * Hello服务实现
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-25 16:58
+ */
+@Service
+@Component
+@Slf4j
+public class HelloServiceImpl implements HelloService {
+ /**
+ * 问好
+ *
+ * @param name 姓名
+ * @return 问好
+ */
+ @Override
+ public String sayHello(String name) {
+ log.info("someone is calling me......");
+ return "say hello to: " + name;
+ }
+}
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-provider/src/main/resources/application.yml b/demo-dubbo/dubbo-provider/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-provider/src/main/resources/application.yml
rename to demo-dubbo/dubbo-provider/src/main/resources/application.yml
diff --git a/spring-boot-demo-dubbo/spring-boot-demo-dubbo-provider/src/test/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplicationTests.java b/demo-dubbo/dubbo-provider/src/test/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplicationTests.java
similarity index 100%
rename from spring-boot-demo-dubbo/spring-boot-demo-dubbo-provider/src/test/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplicationTests.java
rename to demo-dubbo/dubbo-provider/src/test/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplicationTests.java
diff --git a/demo-dubbo/pom.xml b/demo-dubbo/pom.xml
new file mode 100644
index 0000000..3d703b3
--- /dev/null
+++ b/demo-dubbo/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ demo-dubbo
+ 1.0.0-SNAPSHOT
+
+ dubbo-common
+ dubbo-provider
+ dubbo-consumer
+
+ pom
+
+ demo-dubbo
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 2.0.0
+ 0.10
+
+
+
diff --git a/spring-boot-demo-dynamic-datasource/.gitignore b/demo-dynamic-datasource/.gitignore
similarity index 100%
rename from spring-boot-demo-dynamic-datasource/.gitignore
rename to demo-dynamic-datasource/.gitignore
diff --git a/demo-dynamic-datasource/README.md b/demo-dynamic-datasource/README.md
new file mode 100644
index 0000000..7c0e2b7
--- /dev/null
+++ b/demo-dynamic-datasource/README.md
@@ -0,0 +1,669 @@
+# spring-boot-demo-dynamic-datasource
+
+> 此 demo 主要演示了 Spring Boot 项目如何通过接口`动态添加/删除`数据源,添加数据源之后如何`动态切换`数据源,然后使用 mybatis 查询切换后的数据源的数据。
+
+## 1. 环境准备
+
+1. 执行 db 目录下的SQL脚本
+2. 在默认数据源下执行 `init.sql`
+3. 在所有数据源分别执行 `user.sql`
+
+## 2. 主要代码
+
+### 2.1.pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-dynamic-datasource
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-dynamic-datasource
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+ tk.mybatis
+ mapper-spring-boot-starter
+ 2.1.5
+
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ spring-boot-demo-dynamic-datasource
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+### 2.2. 基础配置类
+
+- DatasourceConfiguration.java
+
+> 这个类主要是通过 `DataSourceBuilder` 去构建一个我们自定义的数据源,将其放入 Spring 容器里
+
+```java
+/**
+ *
+ * 数据源配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 10:27
+ */
+@Configuration
+public class DatasourceConfiguration {
+
+ @Bean
+ @ConfigurationProperties(prefix = "spring.datasource")
+ public DataSource dataSource() {
+ DataSourceBuilder> dataSourceBuilder = DataSourceBuilder.create();
+ dataSourceBuilder.type(DynamicDataSource.class);
+ return dataSourceBuilder.build();
+ }
+}
+```
+
+- MybatisConfiguration.java
+
+> 这个类主要是将我们上一步构建出来的数据源配置到 Mybatis 的 `SqlSessionFactory` 里
+
+```java
+/**
+ *
+ * mybatis配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:20
+ */
+@Configuration
+@MapperScan(basePackages = "com.xkcoding.dynamicdatasource.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
+public class MybatisConfiguration {
+ /**
+ * 创建会话工厂。
+ *
+ * @param dataSource 数据源
+ * @return 会话工厂
+ */
+ @Bean(name = "sqlSessionFactory")
+ @SneakyThrows
+ public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
+ SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
+ bean.setDataSource(dataSource);
+ return bean.getObject();
+ }
+}
+```
+
+### 2.3. 动态数据源主要逻辑
+
+- DatasourceConfigContextHolder.java
+
+> 该类主要用于绑定当前线程所使用的数据源 id,通过 ThreadLocal 保证同一线程内不可被修改
+
+```java
+/**
+ *
+ * 数据源标识管理
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:16
+ */
+public class DatasourceConfigContextHolder {
+ private static final ThreadLocal DATASOURCE_HOLDER = ThreadLocal.withInitial(() -> DatasourceHolder.DEFAULT_ID);
+
+ /**
+ * 设置默认数据源
+ */
+ public static void setDefaultDatasource() {
+ DATASOURCE_HOLDER.remove();
+ setCurrentDatasourceConfig(DatasourceHolder.DEFAULT_ID);
+ }
+
+ /**
+ * 获取当前数据源配置id
+ *
+ * @return 数据源配置id
+ */
+ public static Long getCurrentDatasourceConfig() {
+ return DATASOURCE_HOLDER.get();
+ }
+
+ /**
+ * 设置当前数据源配置id
+ *
+ * @param id 数据源配置id
+ */
+ public static void setCurrentDatasourceConfig(Long id) {
+ DATASOURCE_HOLDER.set(id);
+ }
+
+}
+```
+
+- DynamicDataSource.java
+
+> 该类继承 `com.zaxxer.hikari.HikariDataSource`,主要用于动态切换数据源连接。
+
+```java
+/**
+ *
+ * 动态数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 10:41
+ */
+@Slf4j
+public class DynamicDataSource extends HikariDataSource {
+ @Override
+ public Connection getConnection() throws SQLException {
+ // 获取当前数据源 id
+ Long id = DatasourceConfigContextHolder.getCurrentDatasourceConfig();
+ // 根据当前id获取数据源
+ HikariDataSource datasource = DatasourceHolder.INSTANCE.getDatasource(id);
+
+ if (null == datasource) {
+ datasource = initDatasource(id);
+ }
+
+ return datasource.getConnection();
+ }
+
+ /**
+ * 初始化数据源
+ * @param id 数据源id
+ * @return 数据源
+ */
+ private HikariDataSource initDatasource(Long id) {
+ HikariDataSource dataSource = new HikariDataSource();
+
+ // 判断是否是默认数据源
+ if (DatasourceHolder.DEFAULT_ID.equals(id)) {
+ // 默认数据源根据 application.yml 配置的生成
+ DataSourceProperties properties = SpringUtil.getBean(DataSourceProperties.class);
+ dataSource.setJdbcUrl(properties.getUrl());
+ dataSource.setUsername(properties.getUsername());
+ dataSource.setPassword(properties.getPassword());
+ dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
+ } else {
+ // 不是默认数据源,通过缓存获取对应id的数据源的配置
+ DatasourceConfig datasourceConfig = DatasourceConfigCache.INSTANCE.getConfig(id);
+
+ if (datasourceConfig == null) {
+ throw new RuntimeException("无此数据源");
+ }
+
+ dataSource.setJdbcUrl(datasourceConfig.buildJdbcUrl());
+ dataSource.setUsername(datasourceConfig.getUsername());
+ dataSource.setPassword(datasourceConfig.getPassword());
+ dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
+ }
+ // 将创建的数据源添加到数据源管理器中,绑定当前线程
+ DatasourceHolder.INSTANCE.addDatasource(id, dataSource);
+ return dataSource;
+ }
+}
+```
+
+- DatasourceScheduler.java
+
+> 该类主要用于调度任务
+
+```java
+/**
+ *
+ * 数据源缓存释放调度器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:42
+ */
+public enum DatasourceScheduler {
+ /**
+ * 当前实例
+ */
+ INSTANCE;
+
+ private AtomicInteger cacheTaskNumber = new AtomicInteger(1);
+ private ScheduledExecutorService scheduler;
+
+ DatasourceScheduler() {
+ create();
+ }
+
+ private void create() {
+ this.shutdown();
+ this.scheduler = new ScheduledThreadPoolExecutor(10, r -> new Thread(r, String.format("Datasource-Release-Task-%s", cacheTaskNumber.getAndIncrement())));
+ }
+
+ private void shutdown() {
+ if (null != this.scheduler) {
+ this.scheduler.shutdown();
+ }
+ }
+
+ public void schedule(Runnable task,long delay){
+ this.scheduler.scheduleAtFixedRate(task, delay, delay, TimeUnit.MILLISECONDS);
+ }
+
+}
+```
+
+- DatasourceManager.java
+
+> 该类主要用于管理数据源,记录数据源最后使用时间,同时判断是否长时间未使用,超过一定时间未使用,会被释放连接
+
+```java
+/**
+ *
+ * 数据源管理类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:27
+ */
+public class DatasourceManager {
+ /**
+ * 默认释放时间
+ */
+ private static final Long DEFAULT_RELEASE = 10L;
+
+ /**
+ * 数据源
+ */
+ @Getter
+ private HikariDataSource dataSource;
+
+ /**
+ * 上一次使用时间
+ */
+ private LocalDateTime lastUseTime;
+
+ public DatasourceManager(HikariDataSource dataSource) {
+ this.dataSource = dataSource;
+ this.lastUseTime = LocalDateTime.now();
+ }
+
+ /**
+ * 是否已过期,如果过期则关闭数据源
+ *
+ * @return 是否过期,{@code true} 过期,{@code false} 未过期
+ */
+ public boolean isExpired() {
+ if (LocalDateTime.now().isBefore(this.lastUseTime.plusMinutes(DEFAULT_RELEASE))) {
+ return false;
+ }
+ this.dataSource.close();
+ return true;
+ }
+
+ /**
+ * 刷新上次使用时间
+ */
+ public void refreshTime() {
+ this.lastUseTime = LocalDateTime.now();
+ }
+}
+```
+
+- DatasourceHolder.java
+
+> 该类主要用于管理数据源,同时通过 `DatasourceScheduler` 定时检查数据源是否长时间未使用,超时则释放连接
+
+```java
+/**
+ *
+ * 数据源管理
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:23
+ */
+public enum DatasourceHolder {
+ /**
+ * 当前实例
+ */
+ INSTANCE;
+
+ /**
+ * 启动执行,定时5分钟清理一次
+ */
+ DatasourceHolder() {
+ DatasourceScheduler.INSTANCE.schedule(this::clearExpiredDatasource, 5 * 60 * 1000);
+ }
+
+ /**
+ * 默认数据源的id
+ */
+ public static final Long DEFAULT_ID = -1L;
+
+ /**
+ * 管理动态数据源列表。
+ */
+ private static final Map DATASOURCE_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * 添加动态数据源
+ *
+ * @param id 数据源id
+ * @param dataSource 数据源
+ */
+ public synchronized void addDatasource(Long id, HikariDataSource dataSource) {
+ DatasourceManager datasourceManager = new DatasourceManager(dataSource);
+ DATASOURCE_CACHE.put(id, datasourceManager);
+ }
+
+ /**
+ * 查询动态数据源
+ *
+ * @param id 数据源id
+ * @return 数据源
+ */
+ public synchronized HikariDataSource getDatasource(Long id) {
+ if (DATASOURCE_CACHE.containsKey(id)) {
+ DatasourceManager datasourceManager = DATASOURCE_CACHE.get(id);
+ datasourceManager.refreshTime();
+ return datasourceManager.getDataSource();
+ }
+ return null;
+ }
+
+ /**
+ * 清除超时的数据源
+ */
+ public synchronized void clearExpiredDatasource() {
+ DATASOURCE_CACHE.forEach((k, v) -> {
+ // 排除默认数据源
+ if (!DEFAULT_ID.equals(k)) {
+ if (v.isExpired()) {
+ DATASOURCE_CACHE.remove(k);
+ }
+ }
+ });
+ }
+
+ /**
+ * 清除动态数据源
+ * @param id 数据源id
+ */
+ public synchronized void removeDatasource(Long id) {
+ if (DATASOURCE_CACHE.containsKey(id)) {
+ // 关闭数据源
+ DATASOURCE_CACHE.get(id).getDataSource().close();
+ // 移除缓存
+ DATASOURCE_CACHE.remove(id);
+ }
+ }
+}
+```
+
+- DatasourceConfigCache.java
+
+> 该类主要用于缓存数据源的配置,用户生成数据源时,获取数据源连接参数
+
+```java
+/**
+ *
+ * 数据源配置缓存
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:13
+ */
+public enum DatasourceConfigCache {
+ /**
+ * 当前实例
+ */
+ INSTANCE;
+
+ /**
+ * 管理动态数据源列表。
+ */
+ private static final Map CONFIG_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * 添加数据源配置
+ *
+ * @param id 数据源配置id
+ * @param config 数据源配置
+ */
+ public synchronized void addConfig(Long id, DatasourceConfig config) {
+ CONFIG_CACHE.put(id, config);
+ }
+
+ /**
+ * 查询数据源配置
+ *
+ * @param id 数据源配置id
+ * @return 数据源配置
+ */
+ public synchronized DatasourceConfig getConfig(Long id) {
+ if (CONFIG_CACHE.containsKey(id)) {
+ return CONFIG_CACHE.get(id);
+ }
+ return null;
+ }
+
+ /**
+ * 清除数据源配置
+ */
+ public synchronized void removeConfig(Long id) {
+ CONFIG_CACHE.remove(id);
+ // 同步清除 DatasourceHolder 对应的数据源
+ DatasourceHolder.INSTANCE.removeDatasource(id);
+ }
+}
+```
+
+### 2.4. 启动类
+
+> 启动后,使用默认数据源查询数据源配置列表,将其缓存到 `DatasourceConfigCache` 里,以供后续使用
+
+```java
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:57
+ */
+@SpringBootApplication
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class SpringBootDemoDynamicDatasourceApplication implements CommandLineRunner {
+ private final DatasourceConfigMapper configMapper;
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDynamicDatasourceApplication.class, args);
+ }
+
+ @Override
+ public void run(String... args) {
+ // 设置默认的数据源
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ // 查询所有数据库配置列表
+ List datasourceConfigs = configMapper.selectAll();
+ System.out.println("加载其余数据源配置列表: " + datasourceConfigs);
+ // 将数据库配置加入缓存
+ datasourceConfigs.forEach(config -> DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config));
+ }
+}
+```
+
+### 2.5. 其余代码参考 demo
+
+## 3. 测试
+
+启动项目,可以看到控制台读取到数据库已配置的数据源信息
+
+![image-20190905164824155](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062351.png)
+
+通过 PostMan 等工具测试
+
+- 默认数据源查询
+
+![image-20190905165240373](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062353.png)
+
+- 根据数据源id为1的数据源查询
+
+![image-20190905165323097](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062354.png)
+
+- 根据数据源id为2的数据源查询
+
+![image-20190905165350355](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062355.png)
+
+- 可以通过测试数据源的`增加/删除`,再去查询对应数据源的数据
+
+> 删除数据源:
+>
+> - DELETE http://localhost:8080/config/{id}
+>
+> 新增数据源:
+>
+> - POST http://localhost:8080/config
+>
+> - 参数:
+>
+> ```json
+> {
+> "host": "数据库IP",
+> "port": 3306,
+> "username": "用户名",
+> "password": "密码",
+> "database": "数据库"
+> }
+> ```
+
+## 4. 优化
+
+如上测试,我们只需要通过在 header 里传递数据源的参数,即可做到动态切换数据源,怎么做到的呢?
+
+答案就是 `AOP`
+
+```java
+/**
+ *
+ * 数据源选择器切面
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:52
+ */
+@Aspect
+@Component
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class DatasourceSelectorAspect {
+ @Pointcut("execution(public * com.xkcoding.dynamic.datasource.controller.*.*(..))")
+ public void datasourcePointcut() {
+ }
+
+ /**
+ * 前置操作,拦截具体请求,获取header里的数据源id,设置线程变量里,用于后续切换数据源
+ */
+ @Before("datasourcePointcut()")
+ public void doBefore(JoinPoint joinPoint) {
+ Signature signature = joinPoint.getSignature();
+ MethodSignature methodSignature = (MethodSignature) signature;
+ Method method = methodSignature.getMethod();
+
+ // 排除不可切换数据源的方法
+ DefaultDatasource annotation = method.getAnnotation(DefaultDatasource.class);
+ if (null != annotation) {
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ } else {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
+ HttpServletRequest request = attributes.getRequest();
+ String configIdInHeader = request.getHeader("Datasource-Config-Id");
+ if (StringUtils.hasText(configIdInHeader)) {
+ long configId = Long.parseLong(configIdInHeader);
+ DatasourceConfigContextHolder.setCurrentDatasourceConfig(configId);
+ } else {
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ }
+ }
+ }
+
+ /**
+ * 后置操作,设置回默认的数据源id
+ */
+ @AfterReturning("datasourcePointcut()")
+ public void doAfter() {
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ }
+
+}
+```
+
+此时需要考虑,我们是否每个方法都允许用户去切换数据源呢?答案肯定是不行的,所以我们定义了一个注解去标识,当前方法仅可以使用默认数据源。
+
+```java
+/**
+ *
+ * 用户标识仅可以使用默认数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:37
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DefaultDatasource {
+}
+```
+
+完结,撒花✿✿ヽ(°▽°)ノ✿
diff --git a/spring-boot-demo-dynamic-datasource/db/init.sql b/demo-dynamic-datasource/db/init.sql
similarity index 100%
rename from spring-boot-demo-dynamic-datasource/db/init.sql
rename to demo-dynamic-datasource/db/init.sql
diff --git a/spring-boot-demo-dynamic-datasource/db/user.sql b/demo-dynamic-datasource/db/user.sql
similarity index 100%
rename from spring-boot-demo-dynamic-datasource/db/user.sql
rename to demo-dynamic-datasource/db/user.sql
diff --git a/demo-dynamic-datasource/pom.xml b/demo-dynamic-datasource/pom.xml
new file mode 100644
index 0000000..af81136
--- /dev/null
+++ b/demo-dynamic-datasource/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+ demo-dynamic-datasource
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-dynamic-datasource
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+ tk.mybatis
+ mapper-spring-boot-starter
+ 2.1.5
+
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ demo-dynamic-datasource
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java
new file mode 100644
index 0000000..ef3b5e3
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java
@@ -0,0 +1,42 @@
+package com.xkcoding.dynamic.datasource;
+
+import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigCache;
+import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigContextHolder;
+import com.xkcoding.dynamic.datasource.mapper.DatasourceConfigMapper;
+import com.xkcoding.dynamic.datasource.model.DatasourceConfig;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+import java.util.List;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:57
+ */
+@SpringBootApplication
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class SpringBootDemoDynamicDatasourceApplication implements CommandLineRunner {
+ private final DatasourceConfigMapper configMapper;
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoDynamicDatasourceApplication.class, args);
+ }
+
+ @Override
+ public void run(String... args) {
+ // 设置默认的数据源
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ // 查询所有数据库配置列表
+ List datasourceConfigs = configMapper.selectAll();
+ System.out.println("加载其余数据源配置列表: " + datasourceConfigs);
+ // 将数据库配置加入缓存
+ datasourceConfigs.forEach(config -> DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config));
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java
new file mode 100644
index 0000000..2194e1e
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java
@@ -0,0 +1,17 @@
+package com.xkcoding.dynamic.datasource.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ *
+ * 用户标识仅可以使用默认数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:37
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DefaultDatasource {
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java
new file mode 100644
index 0000000..909379b
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java
@@ -0,0 +1,74 @@
+package com.xkcoding.dynamic.datasource.aspect;
+
+import com.xkcoding.dynamic.datasource.annotation.DefaultDatasource;
+import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigContextHolder;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+
+/**
+ *
+ * 数据源选择器切面
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:52
+ */
+@Aspect
+@Component
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class DatasourceSelectorAspect {
+ @Pointcut("execution(public * com.xkcoding.dynamic.datasource.controller.*.*(..))")
+ public void datasourcePointcut() {
+ }
+
+ /**
+ * 前置操作,拦截具体请求,获取header里的数据源id,设置线程变量里,用于后续切换数据源
+ */
+ @Before("datasourcePointcut()")
+ public void doBefore(JoinPoint joinPoint) {
+ Signature signature = joinPoint.getSignature();
+ MethodSignature methodSignature = (MethodSignature) signature;
+ Method method = methodSignature.getMethod();
+
+ // 排除不可切换数据源的方法
+ DefaultDatasource annotation = method.getAnnotation(DefaultDatasource.class);
+ if (null != annotation) {
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ } else {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
+ HttpServletRequest request = attributes.getRequest();
+ String configIdInHeader = request.getHeader("Datasource-Config-Id");
+ if (StringUtils.hasText(configIdInHeader)) {
+ long configId = Long.parseLong(configIdInHeader);
+ DatasourceConfigContextHolder.setCurrentDatasourceConfig(configId);
+ } else {
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ }
+ }
+ }
+
+ /**
+ * 后置操作,设置回默认的数据源id
+ */
+ @AfterReturning("datasourcePointcut()")
+ public void doAfter() {
+ DatasourceConfigContextHolder.setDefaultDatasource();
+ }
+
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java
new file mode 100644
index 0000000..b006fa7
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java
@@ -0,0 +1,29 @@
+package com.xkcoding.dynamic.datasource.config;
+
+import com.xkcoding.dynamic.datasource.datasource.DynamicDataSource;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.sql.DataSource;
+
+/**
+ *
+ * 数据源配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 10:27
+ */
+@Configuration
+public class DatasourceConfiguration {
+
+ @Bean
+ @ConfigurationProperties(prefix = "spring.datasource")
+ public DataSource dataSource() {
+ DataSourceBuilder> dataSourceBuilder = DataSourceBuilder.create();
+ dataSourceBuilder.type(DynamicDataSource.class);
+ return dataSourceBuilder.build();
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java
new file mode 100644
index 0000000..13700d3
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java
@@ -0,0 +1,17 @@
+package com.xkcoding.dynamic.datasource.config;
+
+import tk.mybatis.mapper.annotation.RegisterMapper;
+import tk.mybatis.mapper.common.Mapper;
+import tk.mybatis.mapper.common.MySqlMapper;
+
+/**
+ *
+ * 通用 mapper 自定义 mapper 文件
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:23
+ */
+@RegisterMapper
+public interface MyMapper extends Mapper, MySqlMapper {
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java
new file mode 100644
index 0000000..5ac0b47
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java
@@ -0,0 +1,37 @@
+package com.xkcoding.dynamic.datasource.config;
+
+import lombok.SneakyThrows;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tk.mybatis.spring.annotation.MapperScan;
+
+import javax.sql.DataSource;
+
+/**
+ *
+ * mybatis配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:20
+ */
+@Configuration
+@MapperScan(basePackages = "com.xkcoding.dynamic.datasource.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
+public class MybatisConfiguration {
+ /**
+ * 创建会话工厂。
+ *
+ * @param dataSource 数据源
+ * @return 会话工厂
+ */
+ @Bean(name = "sqlSessionFactory")
+ @SneakyThrows
+ public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
+ SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
+ bean.setDataSource(dataSource);
+ return bean.getObject();
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java
new file mode 100644
index 0000000..b6a0cd3
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java
@@ -0,0 +1,44 @@
+package com.xkcoding.dynamic.datasource.controller;
+
+import com.xkcoding.dynamic.datasource.annotation.DefaultDatasource;
+import com.xkcoding.dynamic.datasource.datasource.DatasourceConfigCache;
+import com.xkcoding.dynamic.datasource.mapper.DatasourceConfigMapper;
+import com.xkcoding.dynamic.datasource.model.DatasourceConfig;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ *
+ * 数据源配置 Controller
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:31
+ */
+@RestController
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class DatasourceConfigController {
+ private final DatasourceConfigMapper configMapper;
+
+ /**
+ * 保存
+ */
+ @PostMapping("/config")
+ @DefaultDatasource
+ public DatasourceConfig insertConfig(@RequestBody DatasourceConfig config) {
+ configMapper.insertUseGeneratedKeys(config);
+ DatasourceConfigCache.INSTANCE.addConfig(config.getId(), config);
+ return config;
+ }
+
+ /**
+ * 保存
+ */
+ @DeleteMapping("/config/{id}")
+ @DefaultDatasource
+ public void removeConfig(@PathVariable Long id) {
+ configMapper.deleteByPrimaryKey(id);
+ DatasourceConfigCache.INSTANCE.removeConfig(id);
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java
new file mode 100644
index 0000000..5802bf0
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java
@@ -0,0 +1,33 @@
+package com.xkcoding.dynamic.datasource.controller;
+
+import com.xkcoding.dynamic.datasource.mapper.UserMapper;
+import com.xkcoding.dynamic.datasource.model.User;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ *
+ * 用户 Controller
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:40
+ */
+@RestController
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class UserController {
+ private final UserMapper userMapper;
+
+ /**
+ * 获取用户列表
+ */
+ @GetMapping("/user")
+ public List getUserList() {
+ return userMapper.selectAll();
+ }
+
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java
new file mode 100644
index 0000000..cd834bc
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java
@@ -0,0 +1,58 @@
+package com.xkcoding.dynamic.datasource.datasource;
+
+import com.xkcoding.dynamic.datasource.model.DatasourceConfig;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ * 数据源配置缓存
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 17:13
+ */
+public enum DatasourceConfigCache {
+ /**
+ * 当前实例
+ */
+ INSTANCE;
+
+ /**
+ * 管理动态数据源列表。
+ */
+ private static final Map CONFIG_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * 添加数据源配置
+ *
+ * @param id 数据源配置id
+ * @param config 数据源配置
+ */
+ public synchronized void addConfig(Long id, DatasourceConfig config) {
+ CONFIG_CACHE.put(id, config);
+ }
+
+ /**
+ * 查询数据源配置
+ *
+ * @param id 数据源配置id
+ * @return 数据源配置
+ */
+ public synchronized DatasourceConfig getConfig(Long id) {
+ if (CONFIG_CACHE.containsKey(id)) {
+ return CONFIG_CACHE.get(id);
+ }
+ return null;
+ }
+
+ /**
+ * 清除数据源配置
+ */
+ public synchronized void removeConfig(Long id) {
+ CONFIG_CACHE.remove(id);
+ // 同步清除 DatasourceHolder 对应的数据源
+ DatasourceHolder.INSTANCE.removeDatasource(id);
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java
new file mode 100644
index 0000000..59db781
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java
@@ -0,0 +1,40 @@
+package com.xkcoding.dynamic.datasource.datasource;
+
+/**
+ *
+ * 数据源标识管理
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:16
+ */
+public class DatasourceConfigContextHolder {
+ private static final ThreadLocal DATASOURCE_HOLDER = ThreadLocal.withInitial(() -> DatasourceHolder.DEFAULT_ID);
+
+ /**
+ * 设置默认数据源
+ */
+ public static void setDefaultDatasource() {
+ DATASOURCE_HOLDER.remove();
+ setCurrentDatasourceConfig(DatasourceHolder.DEFAULT_ID);
+ }
+
+ /**
+ * 获取当前数据源配置id
+ *
+ * @return 数据源配置id
+ */
+ public static Long getCurrentDatasourceConfig() {
+ return DATASOURCE_HOLDER.get();
+ }
+
+ /**
+ * 设置当前数据源配置id
+ *
+ * @param id 数据源配置id
+ */
+ public static void setCurrentDatasourceConfig(Long id) {
+ DATASOURCE_HOLDER.set(id);
+ }
+
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java
new file mode 100644
index 0000000..7685a4f
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java
@@ -0,0 +1,92 @@
+package com.xkcoding.dynamic.datasource.datasource;
+
+import com.zaxxer.hikari.HikariDataSource;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ * 数据源管理
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:23
+ */
+public enum DatasourceHolder {
+ /**
+ * 当前实例
+ */
+ INSTANCE;
+
+ /**
+ * 启动执行,定时5分钟清理一次
+ */
+ DatasourceHolder() {
+ DatasourceScheduler.INSTANCE.schedule(this::clearExpiredDatasource, 5 * 60 * 1000);
+ }
+
+ /**
+ * 默认数据源的id
+ */
+ public static final Long DEFAULT_ID = -1L;
+
+ /**
+ * 管理动态数据源列表。
+ */
+ private static final Map DATASOURCE_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * 添加动态数据源
+ *
+ * @param id 数据源id
+ * @param dataSource 数据源
+ */
+ public synchronized void addDatasource(Long id, HikariDataSource dataSource) {
+ DatasourceManager datasourceManager = new DatasourceManager(dataSource);
+ DATASOURCE_CACHE.put(id, datasourceManager);
+ }
+
+ /**
+ * 查询动态数据源
+ *
+ * @param id 数据源id
+ * @return 数据源
+ */
+ public synchronized HikariDataSource getDatasource(Long id) {
+ if (DATASOURCE_CACHE.containsKey(id)) {
+ DatasourceManager datasourceManager = DATASOURCE_CACHE.get(id);
+ datasourceManager.refreshTime();
+ return datasourceManager.getDataSource();
+ }
+ return null;
+ }
+
+ /**
+ * 清除超时的数据源
+ */
+ public synchronized void clearExpiredDatasource() {
+ DATASOURCE_CACHE.forEach((k, v) -> {
+ // 排除默认数据源
+ if (!DEFAULT_ID.equals(k)) {
+ if (v.isExpired()) {
+ DATASOURCE_CACHE.remove(k);
+ }
+ }
+ });
+ }
+
+ /**
+ * 清除动态数据源
+ *
+ * @param id 数据源id
+ */
+ public synchronized void removeDatasource(Long id) {
+ if (DATASOURCE_CACHE.containsKey(id)) {
+ // 关闭数据源
+ DATASOURCE_CACHE.get(id).getDataSource().close();
+ // 移除缓存
+ DATASOURCE_CACHE.remove(id);
+ }
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java
new file mode 100644
index 0000000..de46953
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java
@@ -0,0 +1,57 @@
+package com.xkcoding.dynamic.datasource.datasource;
+
+import com.zaxxer.hikari.HikariDataSource;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+/**
+ *
+ * 数据源管理类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:27
+ */
+public class DatasourceManager {
+ /**
+ * 默认释放时间
+ */
+ private static final Long DEFAULT_RELEASE = 10L;
+
+ /**
+ * 数据源
+ */
+ @Getter
+ private HikariDataSource dataSource;
+
+ /**
+ * 上一次使用时间
+ */
+ private LocalDateTime lastUseTime;
+
+ public DatasourceManager(HikariDataSource dataSource) {
+ this.dataSource = dataSource;
+ this.lastUseTime = LocalDateTime.now();
+ }
+
+ /**
+ * 是否已过期,如果过期则关闭数据源
+ *
+ * @return 是否过期,{@code true} 过期,{@code false} 未过期
+ */
+ public boolean isExpired() {
+ if (LocalDateTime.now().isBefore(this.lastUseTime.plusMinutes(DEFAULT_RELEASE))) {
+ return false;
+ }
+ this.dataSource.close();
+ return true;
+ }
+
+ /**
+ * 刷新上次使用时间
+ */
+ public void refreshTime() {
+ this.lastUseTime = LocalDateTime.now();
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java
new file mode 100644
index 0000000..3832f1a
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java
@@ -0,0 +1,44 @@
+package com.xkcoding.dynamic.datasource.datasource;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ *
+ * 数据源缓存释放调度器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 14:42
+ */
+public enum DatasourceScheduler {
+ /**
+ * 当前实例
+ */
+ INSTANCE;
+
+ private AtomicInteger cacheTaskNumber = new AtomicInteger(1);
+ private ScheduledExecutorService scheduler;
+
+ DatasourceScheduler() {
+ create();
+ }
+
+ private void create() {
+ this.shutdown();
+ this.scheduler = new ScheduledThreadPoolExecutor(10, r -> new Thread(r, String.format("Datasource-Release-Task-%s", cacheTaskNumber.getAndIncrement())));
+ }
+
+ private void shutdown() {
+ if (null != this.scheduler) {
+ this.scheduler.shutdown();
+ }
+ }
+
+ public void schedule(Runnable task, long delay) {
+ this.scheduler.scheduleAtFixedRate(task, delay, delay, TimeUnit.MILLISECONDS);
+ }
+
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java
new file mode 100644
index 0000000..4a85229
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java
@@ -0,0 +1,70 @@
+package com.xkcoding.dynamic.datasource.datasource;
+
+import com.xkcoding.dynamic.datasource.model.DatasourceConfig;
+import com.xkcoding.dynamic.datasource.utils.SpringUtil;
+import com.zaxxer.hikari.HikariDataSource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ *
+ * 动态数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 10:41
+ */
+@Slf4j
+public class DynamicDataSource extends HikariDataSource {
+ @Override
+ public Connection getConnection() throws SQLException {
+ // 获取当前数据源 id
+ Long id = DatasourceConfigContextHolder.getCurrentDatasourceConfig();
+ // 根据当前id获取数据源
+ HikariDataSource datasource = DatasourceHolder.INSTANCE.getDatasource(id);
+
+ if (null == datasource) {
+ datasource = initDatasource(id);
+ }
+
+ return datasource.getConnection();
+ }
+
+ /**
+ * 初始化数据源
+ *
+ * @param id 数据源id
+ * @return 数据源
+ */
+ private HikariDataSource initDatasource(Long id) {
+ HikariDataSource dataSource = new HikariDataSource();
+
+ // 判断是否是默认数据源
+ if (DatasourceHolder.DEFAULT_ID.equals(id)) {
+ // 默认数据源根据 application.yml 配置的生成
+ DataSourceProperties properties = SpringUtil.getBean(DataSourceProperties.class);
+ dataSource.setJdbcUrl(properties.getUrl());
+ dataSource.setUsername(properties.getUsername());
+ dataSource.setPassword(properties.getPassword());
+ dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
+ } else {
+ // 不是默认数据源,通过缓存获取对应id的数据源的配置
+ DatasourceConfig datasourceConfig = DatasourceConfigCache.INSTANCE.getConfig(id);
+
+ if (datasourceConfig == null) {
+ throw new RuntimeException("无此数据源");
+ }
+
+ dataSource.setJdbcUrl(datasourceConfig.buildJdbcUrl());
+ dataSource.setUsername(datasourceConfig.getUsername());
+ dataSource.setPassword(datasourceConfig.getPassword());
+ dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
+ }
+ // 将创建的数据源添加到数据源管理器中,绑定当前线程
+ DatasourceHolder.INSTANCE.addDatasource(id, dataSource);
+ return dataSource;
+ }
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java
new file mode 100644
index 0000000..a842e53
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java
@@ -0,0 +1,17 @@
+package com.xkcoding.dynamic.datasource.mapper;
+
+import com.xkcoding.dynamic.datasource.config.MyMapper;
+import com.xkcoding.dynamic.datasource.model.DatasourceConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ *
+ * 数据源配置 Mapper
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:20
+ */
+@Mapper
+public interface DatasourceConfigMapper extends MyMapper {
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java
new file mode 100644
index 0000000..a0b7b45
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java
@@ -0,0 +1,17 @@
+package com.xkcoding.dynamic.datasource.mapper;
+
+import com.xkcoding.dynamic.datasource.config.MyMapper;
+import com.xkcoding.dynamic.datasource.model.User;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ *
+ * 用户 Mapper
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:49
+ */
+@Mapper
+public interface UserMapper extends MyMapper {
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java
new file mode 100644
index 0000000..fc69d45
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java
@@ -0,0 +1,69 @@
+package com.xkcoding.dynamic.datasource.model;
+
+import lombok.Data;
+
+import javax.persistence.Column;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ *
+ * 数据源配置表
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 10:58
+ */
+@Data
+@Table(name = "datasource_config")
+public class DatasourceConfig implements Serializable {
+ /**
+ * 主键
+ */
+ @Id
+ @Column(name = "`id`")
+ @GeneratedValue(generator = "JDBC")
+ private Long id;
+
+ /**
+ * 数据库地址
+ */
+ @Column(name = "`host`")
+ private String host;
+
+ /**
+ * 数据库端口
+ */
+ @Column(name = "`port`")
+ private Integer port;
+
+ /**
+ * 数据库用户名
+ */
+ @Column(name = "`username`")
+ private String username;
+
+ /**
+ * 数据库密码
+ */
+ @Column(name = "`password`")
+ private String password;
+
+ /**
+ * 数据库名称
+ */
+ @Column(name = "`database`")
+ private String database;
+
+ /**
+ * 构造JDBC URL
+ *
+ * @return JDBC URL
+ */
+ public String buildJdbcUrl() {
+ return String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&characterEncoding=utf-8&useSSL=false", this.host, this.port, this.database);
+ }
+
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java
new file mode 100644
index 0000000..e7249d7
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java
@@ -0,0 +1,35 @@
+package com.xkcoding.dynamic.datasource.model;
+
+import lombok.Data;
+
+import javax.persistence.Column;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ *
+ * 用户
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:41
+ */
+@Data
+@Table(name = "test_user")
+public class User implements Serializable {
+ /**
+ * 主键
+ */
+ @Id
+ @Column(name = "`id`")
+ @GeneratedValue(generator = "JDBC")
+ private Long id;
+
+ /**
+ * 姓名
+ */
+ @Column(name = "`name`")
+ private String name;
+}
diff --git a/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java
new file mode 100644
index 0000000..a718417
--- /dev/null
+++ b/demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java
@@ -0,0 +1,86 @@
+package com.xkcoding.dynamic.datasource.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+/**
+ *
+ * Spring 工具类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-09-04 16:16
+ */
+@Slf4j
+@Service
+@Lazy(false)
+public class SpringUtil implements ApplicationContextAware, DisposableBean {
+
+ private static ApplicationContext applicationContext = null;
+
+ /**
+ * 取得存储在静态变量中的ApplicationContext.
+ */
+ public static ApplicationContext getApplicationContext() {
+ return applicationContext;
+ }
+
+ /**
+ * 实现ApplicationContextAware接口, 注入Context到静态变量中.
+ */
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) {
+ SpringUtil.applicationContext = applicationContext;
+ }
+
+ /**
+ * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+ */
+ @SuppressWarnings("unchecked")
+ public static T getBean(String name) {
+ return (T) applicationContext.getBean(name);
+ }
+
+ /**
+ * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+ */
+ public static T getBean(Class requiredType) {
+ return applicationContext.getBean(requiredType);
+ }
+
+ /**
+ * 清除SpringContextHolder中的ApplicationContext为Null.
+ */
+ public static void clearHolder() {
+ if (log.isDebugEnabled()) {
+ log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
+ }
+ applicationContext = null;
+ }
+
+ /**
+ * 发布事件
+ *
+ * @param event 事件
+ */
+ public static void publishEvent(ApplicationEvent event) {
+ if (applicationContext == null) {
+ return;
+ }
+ applicationContext.publishEvent(event);
+ }
+
+ /**
+ * 实现DisposableBean接口, 在Context关闭时清理静态变量.
+ */
+ @Override
+ public void destroy() {
+ SpringUtil.clearHolder();
+ }
+
+}
diff --git a/spring-boot-demo-dynamic-datasource/src/main/resources/application.yml b/demo-dynamic-datasource/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-dynamic-datasource/src/main/resources/application.yml
rename to demo-dynamic-datasource/src/main/resources/application.yml
diff --git a/spring-boot-demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java b/demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java
similarity index 100%
rename from spring-boot-demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java
rename to demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java
diff --git a/spring-boot-demo-elasticsearch-rest-high-level-client/.gitignore b/demo-elasticsearch-rest-high-level-client/.gitignore
similarity index 100%
rename from spring-boot-demo-elasticsearch-rest-high-level-client/.gitignore
rename to demo-elasticsearch-rest-high-level-client/.gitignore
diff --git a/demo-elasticsearch-rest-high-level-client/README.md b/demo-elasticsearch-rest-high-level-client/README.md
new file mode 100644
index 0000000..b0e631d
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/README.md
@@ -0,0 +1,493 @@
+# spring-boot-demo-elasticsearch-rest-high-level-client
+
+> 此 demo 主要演示了 Spring Boot 如何集成 `elasticsearch-rest-high-level-client` 完成对 `ElasticSearch 7.x` 版本的基本 CURD 操作
+
+## Elasticsearch 升级
+
+先升级到 6.8,索引创建,设置 mapping 等操作加参数:include_type_name=true,然后滚动升级到 7,旧索引可以用 type 访问。具体可以参考:
+
+https://www.elastic.co/cn/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0
+
+https://www.elastic.co/guide/en/elasticsearch/reference/7.0/removal-of-types.html
+
+## 注意
+
+作者编写本 demo 时,ElasticSearch 版本为 `7.3.0`,使用 docker 运行,下面是所有步骤:
+
+1.下载镜像:`docker pull elasticsearch:7.3.0`
+
+2.下载安装 `docker-compose`,参考文档: https://docs.docker.com/compose/install/
+
+```bash
+sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+```
+
+3.编写docker-compose 文件
+
+```yaml
+version: "3"
+
+services:
+ es7:
+ hostname: es7
+ container_name: es7
+ image: elasticsearch:7.3.0
+ volumes:
+ - "/data/es7/logs:/usr/share/es7/logs:rw"
+ - "/data/es7/data:/usr/share/es7/data:rw"
+ restart: on-failure
+ ports:
+ - "9200:9200"
+ - "9300:9300"
+ environment:
+ cluster.name: elasticsearch
+ discovery.type: single-node
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "50m"
+
+```
+4.启动: `docker-compose -f elasticsearch.yaml up -d`
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+
+ spring-boot-demo-elasticsearch-rest-high-level-client
+ spring-boot-demo-elasticsearch-rest-high-level-client
+ Demo project for Spring Boot
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.hibernate.validator
+ hibernate-validator
+ compile
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ org.elasticsearch
+ elasticsearch
+ 7.3.0
+
+
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-client
+ 7.3.0
+
+
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-high-level-client
+ 7.3.0
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-client
+
+
+ org.elasticsearch
+ elasticsearch
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+
+ spring-boot-demo-elasticsearch-rest-high-level-client
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## Person.java
+
+> 实体类
+>
+
+```java
+package com.xkcoding.elasticsearch.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * Person
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:04
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person implements Serializable {
+
+ private static final long serialVersionUID = 8510634155374943623L;
+
+ /**
+ * 主键
+ */
+ private Long id;
+
+ /**
+ * 名字
+ */
+ private String name;
+
+ /**
+ * 国家
+ */
+ private String country;
+
+ /**
+ * 年龄
+ */
+ private Integer age;
+
+ /**
+ * 生日
+ */
+ private Date birthday;
+
+ /**
+ * 介绍
+ */
+ private String remark;
+
+}
+```
+
+## PersonService.java
+
+```java
+package com.xkcoding.elasticsearch.service;
+
+import com.xkcoding.elasticsearch.model.Person;
+import org.springframework.lang.Nullable;
+
+import java.util.List;
+
+/**
+ * PersonService
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:07
+ */
+public interface PersonService {
+
+ /**
+ * create Index
+ *
+ * @author fxbin
+ * @param index elasticsearch index name
+ */
+ void createIndex(String index);
+
+ /**
+ * delete Index
+ *
+ * @author fxbin
+ * @param index elasticsearch index name
+ */
+ void deleteIndex(String index);
+
+ /**
+ * insert document source
+ *
+ * @author fxbin
+ * @param index elasticsearch index name
+ * @param list data source
+ */
+ void insert(String index, List list);
+
+ /**
+ * update document source
+ *
+ * @author fxbin
+ * @param index elasticsearch index name
+ * @param list data source
+ */
+ void update(String index, List list);
+
+ /**
+ * delete document source
+ *
+ * @author fxbin
+ * @param person delete data source and allow null object
+ */
+ void delete(String index, @Nullable Person person);
+
+ /**
+ * search all doc records
+ *
+ * @author fxbin
+ * @param index elasticsearch index name
+ * @return person list
+ */
+ List searchList(String index);
+
+}
+```
+
+## PersonServiceImpl.java
+
+> service 实现类型,基本CURD操作
+
+```java
+package com.xkcoding.elasticsearch.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.xkcoding.elasticsearch.model.Person;
+import com.xkcoding.elasticsearch.service.base.BaseElasticsearchService;
+import com.xkcoding.elasticsearch.service.PersonService;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.SearchHit;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * PersonServiceImpl
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:08
+ */
+@Service
+public class PersonServiceImpl extends BaseElasticsearchService implements PersonService {
+
+ @Override
+ public void createIndex(String index) {
+ createIndexRequest(index);
+ }
+
+ @Override
+ public void deleteIndex(String index) {
+ deleteIndexRequest(index);
+ }
+
+ @Override
+ public void insert(String index, List list) {
+
+ try {
+ list.forEach(person -> {
+ IndexRequest request = buildIndexRequest(index, String.valueOf(person.getId()), person);
+ try {
+ client.index(request, COMMON_OPTIONS);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ } finally {
+ try {
+ client.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void update(String index, List list) {
+ list.forEach(person -> {
+ updateRequest(index, String.valueOf(person.getId()), person);
+ });
+ }
+
+ @Override
+ public void delete(String index, Person person) {
+ if (ObjectUtils.isEmpty(person)) {
+ // 如果person 对象为空,则删除全量
+ searchList(index).forEach(p -> {
+ deleteRequest(index, String.valueOf(p.getId()));
+ });
+ }
+ deleteRequest(index, String.valueOf(person.getId()));
+ }
+
+ @Override
+ public List searchList(String index) {
+ SearchResponse searchResponse = search(index);
+ SearchHit[] hits = searchResponse.getHits().getHits();
+ List personList = new ArrayList<>();
+ Arrays.stream(hits).forEach(hit -> {
+ Map sourceAsMap = hit.getSourceAsMap();
+ Person person = BeanUtil.mapToBean(sourceAsMap, Person.class, true);
+ personList.add(person);
+ });
+ return personList;
+ }
+}
+```
+
+
+## ElasticsearchApplicationTests.java
+
+> 主要功能测试,参见service 注释说明
+
+```java
+package com.xkcoding.elasticsearch;
+
+import com.xkcoding.elasticsearch.contants.ElasticsearchConstant;
+import com.xkcoding.elasticsearch.model.Person;
+import com.xkcoding.elasticsearch.service.PersonService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class ElasticsearchApplicationTests {
+
+ @Autowired
+ private PersonService personService;
+
+ /**
+ * 测试删除索引
+ */
+ @Test
+ public void deleteIndexTest() {
+ personService.deleteIndex(ElasticsearchConstant.INDEX_NAME);
+ }
+
+ /**
+ * 测试创建索引
+ */
+ @Test
+ public void createIndexTest() {
+ personService.createIndex(ElasticsearchConstant.INDEX_NAME);
+ }
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void insertTest() {
+ List list = new ArrayList<>();
+ list.add(Person.builder().age(11).birthday(new Date()).country("CN").id(1L).name("哈哈").remark("test1").build());
+ list.add(Person.builder().age(22).birthday(new Date()).country("US").id(2L).name("hiahia").remark("test2").build());
+ list.add(Person.builder().age(33).birthday(new Date()).country("ID").id(3L).name("呵呵").remark("test3").build());
+
+ personService.insert(ElasticsearchConstant.INDEX_NAME, list);
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void updateTest() {
+ Person person = Person.builder().age(33).birthday(new Date()).country("ID_update").id(3L).name("呵呵update").remark("test3_update").build();
+ List list = new ArrayList<>();
+ list.add(person);
+ personService.update(ElasticsearchConstant.INDEX_NAME, list);
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void deleteTest() {
+ personService.delete(ElasticsearchConstant.INDEX_NAME, Person.builder().id(1L).build());
+ }
+
+ /**
+ * 测试查询
+ */
+ @Test
+ public void searchListTest() {
+ List personList = personService.searchList(ElasticsearchConstant.INDEX_NAME);
+ System.out.println(personList);
+ }
+
+}
+```
+
+## 参考
+
+- ElasticSearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
+
+- Java High Level REST Client:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.3/java-rest-high.html
+
diff --git a/demo-elasticsearch-rest-high-level-client/pom.xml b/demo-elasticsearch-rest-high-level-client/pom.xml
new file mode 100644
index 0000000..c71eac8
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ spring-boot-demo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+
+ demo-elasticsearch-rest-high-level-client
+ demo-elasticsearch-rest-high-level-client
+ Demo project for Spring Boot
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.hibernate.validator
+ hibernate-validator
+ compile
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ org.elasticsearch
+ elasticsearch
+ 7.3.0
+
+
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-client
+ 7.3.0
+
+
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-high-level-client
+ 7.3.0
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-client
+
+
+ org.elasticsearch
+ elasticsearch
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+
+ demo-elasticsearch-rest-high-level-client
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/ElasticsearchApplication.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/ElasticsearchApplication.java
new file mode 100644
index 0000000..38969ee
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/ElasticsearchApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.elasticsearch;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * ElasticsearchApplication
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:10
+ */
+@SpringBootApplication
+public class ElasticsearchApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ElasticsearchApplication.class, args);
+ }
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/Result.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/Result.java
new file mode 100644
index 0000000..b0b1225
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/Result.java
@@ -0,0 +1,80 @@
+package com.xkcoding.elasticsearch.common;
+
+import lombok.Data;
+import org.springframework.lang.Nullable;
+
+import java.io.Serializable;
+
+/**
+ * Result
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:44
+ */
+@Data
+public class Result implements Serializable {
+
+ private static final long serialVersionUID = 1696194043024336235L;
+
+ /**
+ * 错误码
+ */
+ private int errcode;
+
+ /**
+ * 错误信息
+ */
+ private String errmsg;
+
+ /**
+ * 响应数据
+ */
+ private T data;
+
+ public Result() {
+ }
+
+ private Result(ResultCode resultCode) {
+ this(resultCode.code, resultCode.msg);
+ }
+
+ private Result(ResultCode resultCode, T data) {
+ this(resultCode.code, resultCode.msg, data);
+ }
+
+ private Result(int errcode, String errmsg) {
+ this(errcode, errmsg, null);
+ }
+
+ private Result(int errcode, String errmsg, T data) {
+ this.errcode = errcode;
+ this.errmsg = errmsg;
+ this.data = data;
+ }
+
+
+ /**
+ * 返回成功
+ *
+ * @param 泛型标记
+ * @return 响应信息 {@code Result}
+ */
+ public static Result success() {
+ return new Result<>(ResultCode.SUCCESS);
+ }
+
+
+ /**
+ * 返回成功-携带数据
+ *
+ * @param data 响应数据
+ * @param 泛型标记
+ * @return 响应信息 {@code Result}
+ */
+ public static Result success(@Nullable T data) {
+ return new Result<>(ResultCode.SUCCESS, data);
+ }
+
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/ResultCode.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/ResultCode.java
new file mode 100644
index 0000000..87c3359
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/ResultCode.java
@@ -0,0 +1,31 @@
+package com.xkcoding.elasticsearch.common;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * ResultCode
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:47
+ */
+@Getter
+@AllArgsConstructor
+public enum ResultCode {
+
+ /**
+ * 接口调用成功
+ */
+ SUCCESS(0, "Request Successful"),
+
+ /**
+ * 服务器暂不可用,建议稍候重试。建议重试次数不超过3次。
+ */
+ FAILURE(-1, "System Busy");
+
+ final int code;
+
+ final String msg;
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchAutoConfiguration.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchAutoConfiguration.java
new file mode 100644
index 0000000..228b09b
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchAutoConfiguration.java
@@ -0,0 +1,95 @@
+package com.xkcoding.elasticsearch.config;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestClientBuilder;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * ElasticsearchAutoConfiguration
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 22:59
+ */
+@Configuration
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+@EnableConfigurationProperties(ElasticsearchProperties.class)
+public class ElasticsearchAutoConfiguration {
+
+ private final ElasticsearchProperties elasticsearchProperties;
+
+ private List httpHosts = new ArrayList<>();
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RestHighLevelClient restHighLevelClient() {
+
+ List clusterNodes = elasticsearchProperties.getClusterNodes();
+ clusterNodes.forEach(node -> {
+ try {
+ String[] parts = StringUtils.split(node, ":");
+ Assert.notNull(parts, "Must defined");
+ Assert.state(parts.length == 2, "Must be defined as 'host:port'");
+ httpHosts.add(new HttpHost(parts[0], Integer.parseInt(parts[1]), elasticsearchProperties.getSchema()));
+ } catch (Exception e) {
+ throw new IllegalStateException("Invalid ES nodes " + "property '" + node + "'", e);
+ }
+ });
+ RestClientBuilder builder = RestClient.builder(httpHosts.toArray(new HttpHost[0]));
+
+ return getRestHighLevelClient(builder, elasticsearchProperties);
+ }
+
+
+ /**
+ * get restHistLevelClient
+ *
+ * @param builder RestClientBuilder
+ * @param elasticsearchProperties elasticsearch default properties
+ * @return {@link org.elasticsearch.client.RestHighLevelClient}
+ * @author fxbin
+ */
+ private static RestHighLevelClient getRestHighLevelClient(RestClientBuilder builder, ElasticsearchProperties elasticsearchProperties) {
+
+ // Callback used the default {@link RequestConfig} being set to the {@link CloseableHttpClient}
+ builder.setRequestConfigCallback(requestConfigBuilder -> {
+ requestConfigBuilder.setConnectTimeout(elasticsearchProperties.getConnectTimeout());
+ requestConfigBuilder.setSocketTimeout(elasticsearchProperties.getSocketTimeout());
+ requestConfigBuilder.setConnectionRequestTimeout(elasticsearchProperties.getConnectionRequestTimeout());
+ return requestConfigBuilder;
+ });
+
+ // Callback used to customize the {@link CloseableHttpClient} instance used by a {@link RestClient} instance.
+ builder.setHttpClientConfigCallback(httpClientBuilder -> {
+ httpClientBuilder.setMaxConnTotal(elasticsearchProperties.getMaxConnectTotal());
+ httpClientBuilder.setMaxConnPerRoute(elasticsearchProperties.getMaxConnectPerRoute());
+ return httpClientBuilder;
+ });
+
+ // Callback used the basic credential auth
+ ElasticsearchProperties.Account account = elasticsearchProperties.getAccount();
+ if (!StringUtils.isEmpty(account.getUsername()) && !StringUtils.isEmpty(account.getUsername())) {
+ final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+
+ credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(account.getUsername(), account.getPassword()));
+ }
+ return new RestHighLevelClient(builder);
+ }
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchProperties.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchProperties.java
new file mode 100644
index 0000000..e8a4e15
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchProperties.java
@@ -0,0 +1,116 @@
+package com.xkcoding.elasticsearch.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * ElasticsearchProperties
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 22:58
+ */
+@Data
+@Builder
+@Component
+@NoArgsConstructor
+@AllArgsConstructor
+@ConfigurationProperties(prefix = "demo.data.elasticsearch")
+public class ElasticsearchProperties {
+
+ /**
+ * 请求协议
+ */
+ private String schema = "http";
+
+ /**
+ * 集群名称
+ */
+ private String clusterName = "elasticsearch";
+
+ /**
+ * 集群节点
+ */
+ @NotNull(message = "集群节点不允许为空")
+ private List clusterNodes = new ArrayList<>();
+
+ /**
+ * 连接超时时间(毫秒)
+ */
+ private Integer connectTimeout = 1000;
+
+ /**
+ * socket 超时时间
+ */
+ private Integer socketTimeout = 30000;
+
+ /**
+ * 连接请求超时时间
+ */
+ private Integer connectionRequestTimeout = 500;
+
+ /**
+ * 每个路由的最大连接数量
+ */
+ private Integer maxConnectPerRoute = 10;
+
+ /**
+ * 最大连接总数量
+ */
+ private Integer maxConnectTotal = 30;
+
+ /**
+ * 索引配置信息
+ */
+ private Index index = new Index();
+
+ /**
+ * 认证账户
+ */
+ private Account account = new Account();
+
+ /**
+ * 索引配置信息
+ */
+ @Data
+ public static class Index {
+
+ /**
+ * 分片数量
+ */
+ private Integer numberOfShards = 3;
+
+ /**
+ * 副本数量
+ */
+ private Integer numberOfReplicas = 2;
+
+ }
+
+ /**
+ * 认证账户
+ */
+ @Data
+ public static class Account {
+
+ /**
+ * 认证用户
+ */
+ private String username;
+
+ /**
+ * 认证密码
+ */
+ private String password;
+
+ }
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/contants/ElasticsearchConstant.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/contants/ElasticsearchConstant.java
new file mode 100644
index 0000000..b1eb64a
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/contants/ElasticsearchConstant.java
@@ -0,0 +1,17 @@
+package com.xkcoding.elasticsearch.contants;
+
+/**
+ * ElasticsearchConstant
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:03
+ */
+public interface ElasticsearchConstant {
+
+ /**
+ * 索引名称
+ */
+ String INDEX_NAME = "person";
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/exception/ElasticsearchException.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/exception/ElasticsearchException.java
new file mode 100644
index 0000000..54d597a
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/exception/ElasticsearchException.java
@@ -0,0 +1,47 @@
+package com.xkcoding.elasticsearch.exception;
+
+import com.xkcoding.elasticsearch.common.ResultCode;
+import lombok.Getter;
+
+/**
+ * ElasticsearchException
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:53
+ */
+public class ElasticsearchException extends RuntimeException {
+
+ @Getter
+ private int errcode;
+
+ @Getter
+ private String errmsg;
+
+ public ElasticsearchException(ResultCode resultCode) {
+ this(resultCode.getCode(), resultCode.getMsg());
+ }
+
+ public ElasticsearchException(String message) {
+ super(message);
+ }
+
+ public ElasticsearchException(Integer errcode, String errmsg) {
+ super(errmsg);
+ this.errcode = errcode;
+ this.errmsg = errmsg;
+ }
+
+ public ElasticsearchException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ElasticsearchException(Throwable cause) {
+ super(cause);
+ }
+
+ public ElasticsearchException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/model/Person.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/model/Person.java
new file mode 100644
index 0000000..4a5cdcf
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/model/Person.java
@@ -0,0 +1,56 @@
+package com.xkcoding.elasticsearch.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * Person
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:04
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person implements Serializable {
+
+ private static final long serialVersionUID = 8510634155374943623L;
+
+ /**
+ * 主键
+ */
+ private Long id;
+
+ /**
+ * 名字
+ */
+ private String name;
+
+ /**
+ * 国家
+ */
+ private String country;
+
+ /**
+ * 年龄
+ */
+ private Integer age;
+
+ /**
+ * 生日
+ */
+ private Date birthday;
+
+ /**
+ * 介绍
+ */
+ private String remark;
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/PersonService.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/PersonService.java
new file mode 100644
index 0000000..c35a351
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/PersonService.java
@@ -0,0 +1,68 @@
+package com.xkcoding.elasticsearch.service;
+
+import com.xkcoding.elasticsearch.model.Person;
+import org.springframework.lang.Nullable;
+
+import java.util.List;
+
+/**
+ * PersonService
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:07
+ */
+public interface PersonService {
+
+ /**
+ * create Index
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ void createIndex(String index);
+
+ /**
+ * delete Index
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ void deleteIndex(String index);
+
+ /**
+ * insert document source
+ *
+ * @param index elasticsearch index name
+ * @param list data source
+ * @author fxbin
+ */
+ void insert(String index, List list);
+
+ /**
+ * update document source
+ *
+ * @param index elasticsearch index name
+ * @param list data source
+ * @author fxbin
+ */
+ void update(String index, List list);
+
+ /**
+ * delete document source
+ *
+ * @param person delete data source and allow null object
+ * @author fxbin
+ */
+ void delete(String index, @Nullable Person person);
+
+ /**
+ * search all doc records
+ *
+ * @param index elasticsearch index name
+ * @return person list
+ * @author fxbin
+ */
+ List searchList(String index);
+
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/base/BaseElasticsearchService.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/base/BaseElasticsearchService.java
new file mode 100644
index 0000000..7a0d6b5
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/base/BaseElasticsearchService.java
@@ -0,0 +1,164 @@
+package com.xkcoding.elasticsearch.service.base;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.xkcoding.elasticsearch.config.ElasticsearchProperties;
+import com.xkcoding.elasticsearch.exception.ElasticsearchException;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
+import org.elasticsearch.action.delete.DeleteRequest;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.update.UpdateRequest;
+import org.elasticsearch.client.HttpAsyncResponseConsumerFactory;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.indices.CreateIndexRequest;
+import org.elasticsearch.client.indices.CreateIndexResponse;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+
+/**
+ * BaseElasticsearchService
+ *
+ * @author fxbin
+ * @version 1.0v
+ * @since 2019-09-16 15:44
+ */
+@Slf4j
+public abstract class BaseElasticsearchService {
+
+ @Resource
+ protected RestHighLevelClient client;
+
+ @Resource
+ private ElasticsearchProperties elasticsearchProperties;
+
+ protected static final RequestOptions COMMON_OPTIONS;
+
+ static {
+ RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
+
+ // 默认缓冲限制为100MB,此处修改为30MB。
+ builder.setHttpAsyncResponseConsumerFactory(new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(30 * 1024 * 1024));
+ COMMON_OPTIONS = builder.build();
+ }
+
+ /**
+ * create elasticsearch index (asyc)
+ *
+ * @param index elasticsearch index
+ * @author fxbin
+ */
+ protected void createIndexRequest(String index) {
+ try {
+ CreateIndexRequest request = new CreateIndexRequest(index);
+ // Settings for this index
+ request.settings(Settings.builder().put("index.number_of_shards", elasticsearchProperties.getIndex().getNumberOfShards()).put("index.number_of_replicas", elasticsearchProperties.getIndex().getNumberOfReplicas()));
+
+ CreateIndexResponse createIndexResponse = client.indices().create(request, COMMON_OPTIONS);
+
+ log.info(" whether all of the nodes have acknowledged the request : {}", createIndexResponse.isAcknowledged());
+ log.info(" Indicates whether the requisite number of shard copies were started for each shard in the index before timing out :{}", createIndexResponse.isShardsAcknowledged());
+ } catch (IOException e) {
+ throw new ElasticsearchException("创建索引 {" + index + "} 失败");
+ }
+ }
+
+ /**
+ * delete elasticsearch index
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ protected void deleteIndexRequest(String index) {
+ DeleteIndexRequest deleteIndexRequest = buildDeleteIndexRequest(index);
+ try {
+ client.indices().delete(deleteIndexRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new ElasticsearchException("删除索引 {" + index + "} 失败");
+ }
+ }
+
+ /**
+ * build DeleteIndexRequest
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ private static DeleteIndexRequest buildDeleteIndexRequest(String index) {
+ return new DeleteIndexRequest(index);
+ }
+
+ /**
+ * build IndexRequest
+ *
+ * @param index elasticsearch index name
+ * @param id request object id
+ * @param object request object
+ * @return {@link org.elasticsearch.action.index.IndexRequest}
+ * @author fxbin
+ */
+ protected static IndexRequest buildIndexRequest(String index, String id, Object object) {
+ return new IndexRequest(index).id(id).source(BeanUtil.beanToMap(object), XContentType.JSON);
+ }
+
+ /**
+ * exec updateRequest
+ *
+ * @param index elasticsearch index name
+ * @param id Document id
+ * @param object request object
+ * @author fxbin
+ */
+ protected void updateRequest(String index, String id, Object object) {
+ try {
+ UpdateRequest updateRequest = new UpdateRequest(index, id).doc(BeanUtil.beanToMap(object), XContentType.JSON);
+ client.update(updateRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new ElasticsearchException("更新索引 {" + index + "} 数据 {" + object + "} 失败");
+ }
+ }
+
+ /**
+ * exec deleteRequest
+ *
+ * @param index elasticsearch index name
+ * @param id Document id
+ * @author fxbin
+ */
+ protected void deleteRequest(String index, String id) {
+ try {
+ DeleteRequest deleteRequest = new DeleteRequest(index, id);
+ client.delete(deleteRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new ElasticsearchException("删除索引 {" + index + "} 数据id {" + id + "} 失败");
+ }
+ }
+
+ /**
+ * search all
+ *
+ * @param index elasticsearch index name
+ * @return {@link SearchResponse}
+ * @author fxbin
+ */
+ protected SearchResponse search(String index) {
+ SearchRequest searchRequest = new SearchRequest(index);
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ searchSourceBuilder.query(QueryBuilders.matchAllQuery());
+ searchRequest.source(searchSourceBuilder);
+ SearchResponse searchResponse = null;
+ try {
+ searchResponse = client.search(searchRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return searchResponse;
+ }
+}
diff --git a/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/impl/PersonServiceImpl.java b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/impl/PersonServiceImpl.java
new file mode 100644
index 0000000..ecbc522
--- /dev/null
+++ b/demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/impl/PersonServiceImpl.java
@@ -0,0 +1,90 @@
+package com.xkcoding.elasticsearch.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.xkcoding.elasticsearch.model.Person;
+import com.xkcoding.elasticsearch.service.PersonService;
+import com.xkcoding.elasticsearch.service.base.BaseElasticsearchService;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.SearchHit;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * PersonServiceImpl
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-09-15 23:08
+ */
+@Service
+public class PersonServiceImpl extends BaseElasticsearchService implements PersonService {
+
+ @Override
+ public void createIndex(String index) {
+ createIndexRequest(index);
+ }
+
+ @Override
+ public void deleteIndex(String index) {
+ deleteIndexRequest(index);
+ }
+
+ @Override
+ public void insert(String index, List list) {
+
+ try {
+ list.forEach(person -> {
+ IndexRequest request = buildIndexRequest(index, String.valueOf(person.getId()), person);
+ try {
+ client.index(request, COMMON_OPTIONS);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ } finally {
+ try {
+ client.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void update(String index, List list) {
+ list.forEach(person -> {
+ updateRequest(index, String.valueOf(person.getId()), person);
+ });
+ }
+
+ @Override
+ public void delete(String index, Person person) {
+ if (ObjectUtils.isEmpty(person)) {
+ // 如果person 对象为空,则删除全量
+ searchList(index).forEach(p -> {
+ deleteRequest(index, String.valueOf(p.getId()));
+ });
+ }
+ deleteRequest(index, String.valueOf(person.getId()));
+ }
+
+ @Override
+ public List searchList(String index) {
+ SearchResponse searchResponse = search(index);
+ SearchHit[] hits = searchResponse.getHits().getHits();
+ List personList = new ArrayList<>();
+ Arrays.stream(hits).forEach(hit -> {
+ Map sourceAsMap = hit.getSourceAsMap();
+ Person person = BeanUtil.mapToBean(sourceAsMap, Person.class, true);
+ personList.add(person);
+ });
+ return personList;
+ }
+}
diff --git a/spring-boot-demo-elasticsearch-rest-high-level-client/src/main/resources/application.yml b/demo-elasticsearch-rest-high-level-client/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-elasticsearch-rest-high-level-client/src/main/resources/application.yml
rename to demo-elasticsearch-rest-high-level-client/src/main/resources/application.yml
diff --git a/spring-boot-demo-elasticsearch-rest-high-level-client/src/test/java/com/xkcoding/elasticsearch/ElasticsearchApplicationTests.java b/demo-elasticsearch-rest-high-level-client/src/test/java/com/xkcoding/elasticsearch/ElasticsearchApplicationTests.java
similarity index 100%
rename from spring-boot-demo-elasticsearch-rest-high-level-client/src/test/java/com/xkcoding/elasticsearch/ElasticsearchApplicationTests.java
rename to demo-elasticsearch-rest-high-level-client/src/test/java/com/xkcoding/elasticsearch/ElasticsearchApplicationTests.java
diff --git a/spring-boot-demo-elasticsearch/.gitignore b/demo-elasticsearch/.gitignore
similarity index 100%
rename from spring-boot-demo-elasticsearch/.gitignore
rename to demo-elasticsearch/.gitignore
diff --git a/demo-elasticsearch/README.md b/demo-elasticsearch/README.md
new file mode 100644
index 0000000..67bb956
--- /dev/null
+++ b/demo-elasticsearch/README.md
@@ -0,0 +1,413 @@
+# spring-boot-demo-elasticsearch
+
+> 此 demo 主要演示了 Spring Boot 如何集成 `spring-boot-starter-data-elasticsearch` 完成对 ElasticSearch 的高级使用技巧,包括创建索引、配置映射、删除索引、增删改查基本操作、复杂查询、高级查询、聚合查询等。
+
+## 注意
+
+作者编写本demo时,ElasticSearch版本为 `6.5.3`,使用 docker 运行,下面是所有步骤:
+
+1. 下载镜像:`docker pull elasticsearch:6.5.3`
+
+2. 运行容器:`docker run -d -p 9200:9200 -p 9300:9300 --name elasticsearch-6.5.3 elasticsearch:6.5.3`
+
+3. 进入容器:`docker exec -it elasticsearch-6.5.3 /bin/bash`
+
+4. 安装 ik 分词器:`./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.5.3/elasticsearch-analysis-ik-6.5.3.zip`
+
+5. 修改 es 配置文件:`vi ./config/elasticsearch.yml
+
+ ```yaml
+ cluster.name: "docker-cluster"
+ network.host: 0.0.0.0
+
+ # minimum_master_nodes need to be explicitly set when bound on a public IP
+ # set to 1 to allow single node clusters
+ # Details: https://github.com/elastic/elasticsearch/pull/17288
+ discovery.zen.minimum_master_nodes: 1
+
+ # just for elasticsearch-head plugin
+ http.cors.enabled: true
+ http.cors.allow-origin: "*"
+ ```
+
+6. 退出容器:`exit`
+
+7. 停止容器:`docker stop elasticsearch-6.5.3`
+
+8. 启动容器:`docker start elasticsearch-6.5.3`
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-elasticsearch
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-elasticsearch
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ spring-boot-demo-elasticsearch
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## Person.java
+
+> 实体类
+>
+> @Document 注解主要声明索引名、类型名、分片数量和备份数量
+>
+> @Field 注解主要声明字段对应ES的类型
+
+```java
+/**
+ *
+ * 用户实体类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 17:29
+ */
+@Document(indexName = EsConsts.INDEX_NAME, type = EsConsts.TYPE_NAME, shards = 1, replicas = 0)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person {
+ /**
+ * 主键
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 名字
+ */
+ @Field(type = FieldType.Keyword)
+ private String name;
+
+ /**
+ * 国家
+ */
+ @Field(type = FieldType.Keyword)
+ private String country;
+
+ /**
+ * 年龄
+ */
+ @Field(type = FieldType.Integer)
+ private Integer age;
+
+ /**
+ * 生日
+ */
+ @Field(type = FieldType.Date)
+ private Date birthday;
+
+ /**
+ * 介绍
+ */
+ @Field(type = FieldType.Text, analyzer = "ik_smart")
+ private String remark;
+}
+```
+
+## PersonRepository.java
+
+```java
+/**
+ *
+ * 用户持久层
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 19:00
+ */
+public interface PersonRepository extends ElasticsearchRepository {
+
+ /**
+ * 根据年龄区间查询
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 满足条件的用户列表
+ */
+ List findByAgeBetween(Integer min, Integer max);
+}
+```
+
+## TemplateTest.java
+
+> 主要测试创建索引、映射配置、删除索引
+
+```java
+/**
+ *
+ * 测试 ElasticTemplate 的创建/删除
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 17:46
+ */
+public class TemplateTest extends SpringBootDemoElasticsearchApplicationTests {
+ @Autowired
+ private ElasticsearchTemplate esTemplate;
+
+ /**
+ * 测试 ElasticTemplate 创建 index
+ */
+ @Test
+ public void testCreateIndex() {
+ // 创建索引,会根据Item类的@Document注解信息来创建
+ esTemplate.createIndex(Person.class);
+
+ // 配置映射,会根据Item类中的id、Field等字段来自动完成映射
+ esTemplate.putMapping(Person.class);
+ }
+
+ /**
+ * 测试 ElasticTemplate 删除 index
+ */
+ @Test
+ public void testDeleteIndex() {
+ esTemplate.deleteIndex(Person.class);
+ }
+}
+```
+
+## PersonRepositoryTest.java
+
+> 主要功能,参见方法上方注释
+
+```java
+/**
+ *
+ * 测试 Repository 操作ES
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 19:03
+ */
+@Slf4j
+public class PersonRepositoryTest extends SpringBootDemoElasticsearchApplicationTests {
+ @Autowired
+ private PersonRepository repo;
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void save() {
+ Person person = new Person(1L, "刘备", "蜀国", 18, DateUtil.parse("1990-01-02 03:04:05"), "刘备(161年-223年6月10日),即汉昭烈帝(221年-223年在位),又称先主,字玄德,东汉末年幽州涿郡涿县(今河北省涿州市)人,西汉中山靖王刘胜之后,三国时期蜀汉开国皇帝、政治家。\n刘备少年时拜卢植为师;早年颠沛流离,备尝艰辛,投靠过多个诸侯,曾参与镇压黄巾起义。先后率军救援北海相孔融、徐州牧陶谦等。陶谦病亡后,将徐州让与刘备。赤壁之战时,刘备与孙权联盟击败曹操,趁势夺取荆州。而后进取益州。于章武元年(221年)在成都称帝,国号汉,史称蜀或蜀汉。《三国志》评刘备的机权干略不及曹操,但其弘毅宽厚,知人待士,百折不挠,终成帝业。刘备也称自己做事“每与操反,事乃成尔”。\n章武三年(223年),刘备病逝于白帝城,终年六十三岁,谥号昭烈皇帝,庙号烈祖,葬惠陵。后世有众多文艺作品以其为主角,在成都武侯祠有昭烈庙为纪念。");
+ Person save = repo.save(person);
+ log.info("【save】= {}", save);
+ }
+
+ /**
+ * 测试批量新增
+ */
+ @Test
+ public void saveList() {
+ List personList = Lists.newArrayList();
+ personList.add(new Person(2L, "曹操", "魏国", 20, DateUtil.parse("1988-01-02 03:04:05"), "曹操(155年-220年3月15日),字孟德,一名吉利,小字阿瞒,沛国谯县(今安徽亳州)人。东汉末年杰出的政治家、军事家、文学家、书法家,三国中曹魏政权的奠基人。\n曹操曾担任东汉丞相,后加封魏王,奠定了曹魏立国的基础。去世后谥号为武王。其子曹丕称帝后,追尊为武皇帝,庙号太祖。\n东汉末年,天下大乱,曹操以汉天子的名义征讨四方,对内消灭二袁、吕布、刘表、马超、韩遂等割据势力,对外降服南匈奴、乌桓、鲜卑等,统一了中国北方,并实行一系列政策恢复经济生产和社会秩序,扩大屯田、兴修水利、奖励农桑、重视手工业、安置流亡人口、实行“租调制”,从而使中原社会渐趋稳定、经济出现转机。黄河流域在曹操统治下,政治渐见清明,经济逐步恢复,阶级压迫稍有减轻,社会风气有所好转。曹操在汉朝的名义下所采取的一些措施具有积极作用。\n曹操军事上精通兵法,重贤爱才,为此不惜一切代价将看中的潜能分子收于麾下;生活上善诗歌,抒发自己的政治抱负,并反映汉末人民的苦难生活,气魄雄伟,慷慨悲凉;散文亦清峻整洁,开启并繁荣了建安文学,给后人留下了宝贵的精神财富,鲁迅评价其为“改造文章的祖师”。同时曹操也擅长书法,唐朝张怀瓘在《书断》将曹操的章草评为“妙品”。"));
+ personList.add(new Person(3L, "孙权", "吴国", 19, DateUtil.parse("1989-01-02 03:04:05"), "孙权(182年-252年5月21日),字仲谋,吴郡富春(今浙江杭州富阳区)人。三国时代孙吴的建立者(229年-252年在位)。\n孙权的父亲孙坚和兄长孙策,在东汉末年群雄割据中打下了江东基业。建安五年(200年),孙策遇刺身亡,孙权继之掌事,成为一方诸侯。建安十三年(208年),与刘备建立孙刘联盟,并于赤壁之战中击败曹操,奠定三国鼎立的基础。建安二十四年(219年),孙权派吕蒙成功袭取刘备的荆州,使领土面积大大增加。\n黄武元年(222年),孙权被魏文帝曹丕册封为吴王,建立吴国。同年,在夷陵之战中大败刘备。黄龙元年(229年),在武昌正式称帝,国号吴,不久后迁都建业。孙权称帝后,设置农官,实行屯田,设置郡县,并继续剿抚山越,促进了江南经济的发展。在此基础上,他又多次派人出海。黄龙二年(230年),孙权派卫温、诸葛直抵达夷州。\n孙权晚年在继承人问题上反复无常,引致群下党争,朝局不稳。太元元年(252年)病逝,享年七十一岁,在位二十四年,谥号大皇帝,庙号太祖,葬于蒋陵。\n孙权亦善书,唐代张怀瓘在《书估》中将其书法列为第三等。"));
+ personList.add(new Person(4L, "诸葛亮", "蜀国", 16, DateUtil.parse("1992-01-02 03:04:05"), "诸葛亮(181年-234年10月8日),字孔明,号卧龙,徐州琅琊阳都(今山东临沂市沂南县)人,三国时期蜀国丞相,杰出的政治家、军事家、外交家、文学家、书法家、发明家。\n早年随叔父诸葛玄到荆州,诸葛玄死后,诸葛亮就在襄阳隆中隐居。后刘备三顾茅庐请出诸葛亮,联孙抗曹,于赤壁之战大败曹军。形成三国鼎足之势,又夺占荆州。建安十六年(211年),攻取益州。继又击败曹军,夺得汉中。蜀章武元年(221年),刘备在成都建立蜀汉政权,诸葛亮被任命为丞相,主持朝政。蜀后主刘禅继位,诸葛亮被封为武乡侯,领益州牧。勤勉谨慎,大小政事必亲自处理,赏罚严明;与东吴联盟,改善和西南各族的关系;实行屯田政策,加强战备。前后六次北伐中原,多以粮尽无功。终因积劳成疾,于蜀建兴十二年(234年)病逝于五丈原(今陕西宝鸡岐山境内),享年54岁。刘禅追封其为忠武侯,后世常以武侯尊称诸葛亮。东晋政权因其军事才能特追封他为武兴王。\n诸葛亮散文代表作有《出师表》《诫子书》等。曾发明木牛流马、孔明灯等,并改造连弩,叫做诸葛连弩,可一弩十矢俱发。诸葛亮一生“鞠躬尽瘁、死而后已”,是中国传统文化中忠臣与智者的代表人物。"));
+ Iterable people = repo.saveAll(personList);
+ log.info("【people】= {}", people);
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void update() {
+ repo.findById(1L).ifPresent(person -> {
+ person.setRemark(person.getRemark() + "\n更新更新更新更新更新");
+ Person save = repo.save(person);
+ log.info("【save】= {}", save);
+ });
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void delete() {
+ // 主键删除
+ repo.deleteById(1L);
+
+ // 对象删除
+ repo.findById(2L).ifPresent(person -> repo.delete(person));
+
+ // 批量删除
+ repo.deleteAll(repo.findAll());
+ }
+
+ /**
+ * 测试普通查询,按生日倒序
+ */
+ @Test
+ public void select() {
+ repo.findAll(Sort.by(Sort.Direction.DESC, "birthday"))
+ .forEach(person -> log.info("{} 生日: {}", person.getName(), DateUtil.formatDateTime(person.getBirthday())));
+ }
+
+ /**
+ * 自定义查询,根据年龄范围查询
+ */
+ @Test
+ public void customSelectRangeOfAge() {
+ repo.findByAgeBetween(18, 19).forEach(person -> log.info("{} 年龄: {}", person.getName(), person.getAge()));
+ }
+
+ /**
+ * 高级查询
+ */
+ @Test
+ public void advanceSelect() {
+ // QueryBuilders 提供了很多静态方法,可以实现大部分查询条件的封装
+ MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "孙权");
+ log.info("【queryBuilder】= {}", queryBuilder.toString());
+
+ repo.search(queryBuilder).forEach(person -> log.info("【person】= {}", person));
+ }
+
+ /**
+ * 自定义高级查询
+ */
+ @Test
+ public void customAdvanceSelect() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 添加基本的分词条件
+ queryBuilder.withQuery(QueryBuilders.matchQuery("remark", "东汉"));
+ // 排序条件
+ queryBuilder.withSort(SortBuilders.fieldSort("age").order(SortOrder.DESC));
+ // 分页条件
+ queryBuilder.withPageable(PageRequest.of(0, 2));
+ Page people = repo.search(queryBuilder.build());
+ log.info("【people】总条数 = {}", people.getTotalElements());
+ log.info("【people】总页数 = {}", people.getTotalPages());
+ people.forEach(person -> log.info("【person】= {},年龄 = {}", person.getName(), person.getAge()));
+ }
+
+ /**
+ * 测试聚合,测试平均年龄
+ */
+ @Test
+ public void agg() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 不查询任何结果
+ queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+ // 平均年龄
+ queryBuilder.addAggregation(AggregationBuilders.avg("avg").field("age"));
+
+ log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+ AggregatedPage people = (AggregatedPage) repo.search(queryBuilder.build());
+ double avgAge = ((InternalAvg) people.getAggregation("avg")).getValue();
+ log.info("【avgAge】= {}", avgAge);
+ }
+
+ /**
+ * 测试高级聚合查询,每个国家的人有几个,每个国家的平均年龄是多少
+ */
+ @Test
+ public void advanceAgg() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 不查询任何结果
+ queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+ // 1. 添加一个新的聚合,聚合类型为terms,聚合名称为country,聚合字段为age
+ queryBuilder.addAggregation(AggregationBuilders.terms("country").field("country")
+ // 2. 在国家聚合桶内进行嵌套聚合,求平均年龄
+ .subAggregation(AggregationBuilders.avg("avg").field("age")));
+
+ log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+ // 3. 查询
+ AggregatedPage people = (AggregatedPage) repo.search(queryBuilder.build());
+
+ // 4. 解析
+ // 4.1. 从结果中取出名为 country 的那个聚合,因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
+ StringTerms country = (StringTerms) people.getAggregation("country");
+ // 4.2. 获取桶
+ List buckets = country.getBuckets();
+ for (StringTerms.Bucket bucket : buckets) {
+ // 4.3. 获取桶中的key,即国家名称 4.4. 获取桶中的文档数量
+ log.info("{} 总共有 {} 人", bucket.getKeyAsString(), bucket.getDocCount());
+ // 4.5. 获取子聚合结果:
+ InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("avg");
+ log.info("平均年龄:{}", avg);
+ }
+ }
+
+}
+```
+
+## 参考
+
+1. ElasticSearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/6.x/getting-started.html
+2. spring-data-elasticsearch 官方文档:https://docs.spring.io/spring-data/elasticsearch/docs/3.1.2.RELEASE/reference/html/
+
diff --git a/demo-elasticsearch/pom.xml b/demo-elasticsearch/pom.xml
new file mode 100644
index 0000000..4b93e31
--- /dev/null
+++ b/demo-elasticsearch/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ demo-elasticsearch
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-elasticsearch
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ demo-elasticsearch
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplication.java b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplication.java
new file mode 100644
index 0000000..2601284
--- /dev/null
+++ b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplication.java
@@ -0,0 +1,21 @@
+package com.xkcoding.elasticsearch;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-27 22:52
+ */
+@SpringBootApplication
+public class SpringBootDemoElasticsearchApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoElasticsearchApplication.class, args);
+ }
+
+}
diff --git a/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/constants/EsConsts.java b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/constants/EsConsts.java
new file mode 100644
index 0000000..a67f42f
--- /dev/null
+++ b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/constants/EsConsts.java
@@ -0,0 +1,21 @@
+package com.xkcoding.elasticsearch.constants;
+
+/**
+ *
+ * ES常量池
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 17:30
+ */
+public interface EsConsts {
+ /**
+ * 索引名称
+ */
+ String INDEX_NAME = "person";
+
+ /**
+ * 类型名称
+ */
+ String TYPE_NAME = "person";
+}
diff --git a/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/model/Person.java b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/model/Person.java
new file mode 100644
index 0000000..1d6f8a6
--- /dev/null
+++ b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/model/Person.java
@@ -0,0 +1,62 @@
+package com.xkcoding.elasticsearch.model;
+
+import com.xkcoding.elasticsearch.constants.EsConsts;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+
+import java.util.Date;
+
+/**
+ *
+ * 用户实体类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 17:29
+ */
+@Document(indexName = EsConsts.INDEX_NAME, type = EsConsts.TYPE_NAME, shards = 1, replicas = 0)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person {
+ /**
+ * 主键
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 名字
+ */
+ @Field(type = FieldType.Keyword)
+ private String name;
+
+ /**
+ * 国家
+ */
+ @Field(type = FieldType.Keyword)
+ private String country;
+
+ /**
+ * 年龄
+ */
+ @Field(type = FieldType.Integer)
+ private Integer age;
+
+ /**
+ * 生日
+ */
+ @Field(type = FieldType.Date)
+ private Date birthday;
+
+ /**
+ * 介绍
+ */
+ @Field(type = FieldType.Text, analyzer = "ik_smart")
+ private String remark;
+}
diff --git a/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/repository/PersonRepository.java b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/repository/PersonRepository.java
new file mode 100644
index 0000000..49f4475
--- /dev/null
+++ b/demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/repository/PersonRepository.java
@@ -0,0 +1,26 @@
+package com.xkcoding.elasticsearch.repository;
+
+import com.xkcoding.elasticsearch.model.Person;
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+
+import java.util.List;
+
+/**
+ *
+ * 用户持久层
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 19:00
+ */
+public interface PersonRepository extends ElasticsearchRepository {
+
+ /**
+ * 根据年龄区间查询
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 满足条件的用户列表
+ */
+ List findByAgeBetween(Integer min, Integer max);
+}
diff --git a/spring-boot-demo-elasticsearch/src/main/resources/application.yml b/demo-elasticsearch/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-elasticsearch/src/main/resources/application.yml
rename to demo-elasticsearch/src/main/resources/application.yml
diff --git a/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplicationTests.java b/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplicationTests.java
new file mode 100644
index 0000000..e25bc50
--- /dev/null
+++ b/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.elasticsearch;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoElasticsearchApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/repository/PersonRepositoryTest.java b/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/repository/PersonRepositoryTest.java
new file mode 100644
index 0000000..c3b22c4
--- /dev/null
+++ b/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/repository/PersonRepositoryTest.java
@@ -0,0 +1,191 @@
+package com.xkcoding.elasticsearch.repository;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.collect.Lists;
+import com.xkcoding.elasticsearch.SpringBootDemoElasticsearchApplicationTests;
+import com.xkcoding.elasticsearch.model.Person;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.index.query.MatchQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
+import org.elasticsearch.search.aggregations.metrics.avg.InternalAvg;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
+import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
+import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
+
+import java.util.List;
+
+/**
+ *
+ * 测试 Repository 操作ES
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 19:03
+ */
+@Slf4j
+public class PersonRepositoryTest extends SpringBootDemoElasticsearchApplicationTests {
+ @Autowired
+ private PersonRepository repo;
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void save() {
+ Person person = new Person(1L, "刘备", "蜀国", 18, DateUtil.parse("1990-01-02 03:04:05"), "刘备(161年-223年6月10日),即汉昭烈帝(221年-223年在位),又称先主,字玄德,东汉末年幽州涿郡涿县(今河北省涿州市)人,西汉中山靖王刘胜之后,三国时期蜀汉开国皇帝、政治家。\n刘备少年时拜卢植为师;早年颠沛流离,备尝艰辛,投靠过多个诸侯,曾参与镇压黄巾起义。先后率军救援北海相孔融、徐州牧陶谦等。陶谦病亡后,将徐州让与刘备。赤壁之战时,刘备与孙权联盟击败曹操,趁势夺取荆州。而后进取益州。于章武元年(221年)在成都称帝,国号汉,史称蜀或蜀汉。《三国志》评刘备的机权干略不及曹操,但其弘毅宽厚,知人待士,百折不挠,终成帝业。刘备也称自己做事“每与操反,事乃成尔”。\n章武三年(223年),刘备病逝于白帝城,终年六十三岁,谥号昭烈皇帝,庙号烈祖,葬惠陵。后世有众多文艺作品以其为主角,在成都武侯祠有昭烈庙为纪念。");
+ Person save = repo.save(person);
+ log.info("【save】= {}", save);
+ }
+
+ /**
+ * 测试批量新增
+ */
+ @Test
+ public void saveList() {
+ List personList = Lists.newArrayList();
+ personList.add(new Person(2L, "曹操", "魏国", 20, DateUtil.parse("1988-01-02 03:04:05"), "曹操(155年-220年3月15日),字孟德,一名吉利,小字阿瞒,沛国谯县(今安徽亳州)人。东汉末年杰出的政治家、军事家、文学家、书法家,三国中曹魏政权的奠基人。\n曹操曾担任东汉丞相,后加封魏王,奠定了曹魏立国的基础。去世后谥号为武王。其子曹丕称帝后,追尊为武皇帝,庙号太祖。\n东汉末年,天下大乱,曹操以汉天子的名义征讨四方,对内消灭二袁、吕布、刘表、马超、韩遂等割据势力,对外降服南匈奴、乌桓、鲜卑等,统一了中国北方,并实行一系列政策恢复经济生产和社会秩序,扩大屯田、兴修水利、奖励农桑、重视手工业、安置流亡人口、实行“租调制”,从而使中原社会渐趋稳定、经济出现转机。黄河流域在曹操统治下,政治渐见清明,经济逐步恢复,阶级压迫稍有减轻,社会风气有所好转。曹操在汉朝的名义下所采取的一些措施具有积极作用。\n曹操军事上精通兵法,重贤爱才,为此不惜一切代价将看中的潜能分子收于麾下;生活上善诗歌,抒发自己的政治抱负,并反映汉末人民的苦难生活,气魄雄伟,慷慨悲凉;散文亦清峻整洁,开启并繁荣了建安文学,给后人留下了宝贵的精神财富,鲁迅评价其为“改造文章的祖师”。同时曹操也擅长书法,唐朝张怀瓘在《书断》将曹操的章草评为“妙品”。"));
+ personList.add(new Person(3L, "孙权", "吴国", 19, DateUtil.parse("1989-01-02 03:04:05"), "孙权(182年-252年5月21日),字仲谋,吴郡富春(今浙江杭州富阳区)人。三国时代孙吴的建立者(229年-252年在位)。\n孙权的父亲孙坚和兄长孙策,在东汉末年群雄割据中打下了江东基业。建安五年(200年),孙策遇刺身亡,孙权继之掌事,成为一方诸侯。建安十三年(208年),与刘备建立孙刘联盟,并于赤壁之战中击败曹操,奠定三国鼎立的基础。建安二十四年(219年),孙权派吕蒙成功袭取刘备的荆州,使领土面积大大增加。\n黄武元年(222年),孙权被魏文帝曹丕册封为吴王,建立吴国。同年,在夷陵之战中大败刘备。黄龙元年(229年),在武昌正式称帝,国号吴,不久后迁都建业。孙权称帝后,设置农官,实行屯田,设置郡县,并继续剿抚山越,促进了江南经济的发展。在此基础上,他又多次派人出海。黄龙二年(230年),孙权派卫温、诸葛直抵达夷州。\n孙权晚年在继承人问题上反复无常,引致群下党争,朝局不稳。太元元年(252年)病逝,享年七十一岁,在位二十四年,谥号大皇帝,庙号太祖,葬于蒋陵。\n孙权亦善书,唐代张怀瓘在《书估》中将其书法列为第三等。"));
+ personList.add(new Person(4L, "诸葛亮", "蜀国", 16, DateUtil.parse("1992-01-02 03:04:05"), "诸葛亮(181年-234年10月8日),字孔明,号卧龙,徐州琅琊阳都(今山东临沂市沂南县)人,三国时期蜀国丞相,杰出的政治家、军事家、外交家、文学家、书法家、发明家。\n早年随叔父诸葛玄到荆州,诸葛玄死后,诸葛亮就在襄阳隆中隐居。后刘备三顾茅庐请出诸葛亮,联孙抗曹,于赤壁之战大败曹军。形成三国鼎足之势,又夺占荆州。建安十六年(211年),攻取益州。继又击败曹军,夺得汉中。蜀章武元年(221年),刘备在成都建立蜀汉政权,诸葛亮被任命为丞相,主持朝政。蜀后主刘禅继位,诸葛亮被封为武乡侯,领益州牧。勤勉谨慎,大小政事必亲自处理,赏罚严明;与东吴联盟,改善和西南各族的关系;实行屯田政策,加强战备。前后六次北伐中原,多以粮尽无功。终因积劳成疾,于蜀建兴十二年(234年)病逝于五丈原(今陕西宝鸡岐山境内),享年54岁。刘禅追封其为忠武侯,后世常以武侯尊称诸葛亮。东晋政权因其军事才能特追封他为武兴王。\n诸葛亮散文代表作有《出师表》《诫子书》等。曾发明木牛流马、孔明灯等,并改造连弩,叫做诸葛连弩,可一弩十矢俱发。诸葛亮一生“鞠躬尽瘁、死而后已”,是中国传统文化中忠臣与智者的代表人物。"));
+ Iterable people = repo.saveAll(personList);
+ log.info("【people】= {}", people);
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void update() {
+ repo.findById(1L).ifPresent(person -> {
+ person.setRemark(person.getRemark() + "\n更新更新更新更新更新");
+ Person save = repo.save(person);
+ log.info("【save】= {}", save);
+ });
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void delete() {
+ // 主键删除
+ repo.deleteById(1L);
+
+ // 对象删除
+ repo.findById(2L).ifPresent(person -> repo.delete(person));
+
+ // 批量删除
+ repo.deleteAll(repo.findAll());
+ }
+
+ /**
+ * 测试普通查询,按生日倒序
+ */
+ @Test
+ public void select() {
+ repo.findAll(Sort.by(Sort.Direction.DESC, "birthday")).forEach(person -> log.info("{} 生日: {}", person.getName(), DateUtil.formatDateTime(person.getBirthday())));
+ }
+
+ /**
+ * 自定义查询,根据年龄范围查询
+ */
+ @Test
+ public void customSelectRangeOfAge() {
+ repo.findByAgeBetween(18, 19).forEach(person -> log.info("{} 年龄: {}", person.getName(), person.getAge()));
+ }
+
+ /**
+ * 高级查询
+ */
+ @Test
+ public void advanceSelect() {
+ // QueryBuilders 提供了很多静态方法,可以实现大部分查询条件的封装
+ MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "孙权");
+ log.info("【queryBuilder】= {}", queryBuilder.toString());
+
+ repo.search(queryBuilder).forEach(person -> log.info("【person】= {}", person));
+ }
+
+ /**
+ * 自定义高级查询
+ */
+ @Test
+ public void customAdvanceSelect() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 添加基本的分词条件
+ queryBuilder.withQuery(QueryBuilders.matchQuery("remark", "东汉"));
+ // 排序条件
+ queryBuilder.withSort(SortBuilders.fieldSort("age").order(SortOrder.DESC));
+ // 分页条件
+ queryBuilder.withPageable(PageRequest.of(0, 2));
+ Page people = repo.search(queryBuilder.build());
+ log.info("【people】总条数 = {}", people.getTotalElements());
+ log.info("【people】总页数 = {}", people.getTotalPages());
+ people.forEach(person -> log.info("【person】= {},年龄 = {}", person.getName(), person.getAge()));
+ }
+
+ /**
+ * 测试聚合,测试平均年龄
+ */
+ @Test
+ public void agg() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 不查询任何结果
+ queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+ // 平均年龄
+ queryBuilder.addAggregation(AggregationBuilders.avg("avg").field("age"));
+
+ log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+ AggregatedPage people = (AggregatedPage) repo.search(queryBuilder.build());
+ double avgAge = ((InternalAvg) people.getAggregation("avg")).getValue();
+ log.info("【avgAge】= {}", avgAge);
+ }
+
+ /**
+ * 测试高级聚合查询,每个国家的人有几个,每个国家的平均年龄是多少
+ */
+ @Test
+ public void advanceAgg() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 不查询任何结果
+ queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+ // 1. 添加一个新的聚合,聚合类型为terms,聚合名称为country,聚合字段为age
+ queryBuilder.addAggregation(AggregationBuilders.terms("country").field("country")
+ // 2. 在国家聚合桶内进行嵌套聚合,求平均年龄
+ .subAggregation(AggregationBuilders.avg("avg").field("age")));
+
+ log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+ // 3. 查询
+ AggregatedPage people = (AggregatedPage) repo.search(queryBuilder.build());
+
+ // 4. 解析
+ // 4.1. 从结果中取出名为 country 的那个聚合,因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
+ StringTerms country = (StringTerms) people.getAggregation("country");
+ // 4.2. 获取桶
+ List buckets = country.getBuckets();
+ for (StringTerms.Bucket bucket : buckets) {
+ // 4.3. 获取桶中的key,即国家名称 4.4. 获取桶中的文档数量
+ log.info("{} 总共有 {} 人", bucket.getKeyAsString(), bucket.getDocCount());
+ // 4.5. 获取子聚合结果:
+ InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("avg");
+ log.info("平均年龄:{}", avg);
+ }
+ }
+
+}
diff --git a/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/template/TemplateTest.java b/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/template/TemplateTest.java
new file mode 100644
index 0000000..6d53aa3
--- /dev/null
+++ b/demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/template/TemplateTest.java
@@ -0,0 +1,40 @@
+package com.xkcoding.elasticsearch.template;
+
+import com.xkcoding.elasticsearch.SpringBootDemoElasticsearchApplicationTests;
+import com.xkcoding.elasticsearch.model.Person;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
+
+/**
+ *
+ * 测试 ElasticTemplate 的创建/删除
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-20 17:46
+ */
+public class TemplateTest extends SpringBootDemoElasticsearchApplicationTests {
+ @Autowired
+ private ElasticsearchTemplate esTemplate;
+
+ /**
+ * 测试 ElasticTemplate 创建 index
+ */
+ @Test
+ public void testCreateIndex() {
+ // 创建索引,会根据Item类的@Document注解信息来创建
+ esTemplate.createIndex(Person.class);
+
+ // 配置映射,会根据Item类中的id、Field等字段来自动完成映射
+ esTemplate.putMapping(Person.class);
+ }
+
+ /**
+ * 测试 ElasticTemplate 删除 index
+ */
+ @Test
+ public void testDeleteIndex() {
+ esTemplate.deleteIndex(Person.class);
+ }
+}
diff --git a/spring-boot-demo-email/.gitignore b/demo-email/.gitignore
similarity index 100%
rename from spring-boot-demo-email/.gitignore
rename to demo-email/.gitignore
diff --git a/demo-email/README.md b/demo-email/README.md
new file mode 100644
index 0000000..85d037e
--- /dev/null
+++ b/demo-email/README.md
@@ -0,0 +1,441 @@
+# spring-boot-demo-email
+
+> 此 demo 主要演示了 Spring Boot 如何整合邮件功能,包括发送简单文本邮件、HTML邮件(包括模板HTML邮件)、附件邮件、静态资源邮件。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-email
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-email
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 2.1.1
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+
+ com.github.ulisesbocchio
+ jasypt-spring-boot-starter
+ ${jasypt.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+
+ spring-boot-demo-email
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+spring:
+ mail:
+ host: smtp.mxhichina.com
+ port: 465
+ username: spring-boot-demo@xkcoding.com
+ # 使用 jasypt 加密密码,使用com.xkcoding.email.PasswordTest.testGeneratePassword 生成加密密码,替换 ENC(加密密码)
+ password: ENC(OT0qGOpXrr1Iog1W+fjOiIDCJdBjHyhy)
+ protocol: smtp
+ test-connection: true
+ default-encoding: UTF-8
+ properties:
+ mail.smtp.auth: true
+ mail.smtp.starttls.enable: true
+ mail.smtp.starttls.required: true
+ mail.smtp.ssl.enable: true
+ mail.display.sendmail: spring-boot-demo
+# 为 jasypt 配置解密秘钥
+jasypt:
+ encryptor:
+ password: spring-boot-demo
+
+```
+
+## MailService.java
+
+```java
+/**
+ *
+ * 邮件接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-21 11:16
+ */
+public interface MailService {
+ /**
+ * 发送文本邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ */
+ void sendSimpleMail(String to, String subject, String content, String... cc);
+
+ /**
+ * 发送HTML邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException;
+
+ /**
+ * 发送带附件的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param filePath 附件地址
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc) throws MessagingException;
+
+ /**
+ * 发送正文中有静态资源的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param rscPath 静态资源地址
+ * @param rscId 静态资源id
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc) throws MessagingException;
+
+}
+```
+
+## MailServiceImpl.java
+
+```java
+/**
+ *
+ * 邮件接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-21 13:49
+ */
+@Service
+public class MailServiceImpl implements MailService {
+ @Autowired
+ private JavaMailSender mailSender;
+ @Value("${spring.mail.username}")
+ private String from;
+
+ /**
+ * 发送文本邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ */
+ @Override
+ public void sendSimpleMail(String to, String subject, String content, String... cc) {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(from);
+ message.setTo(to);
+ message.setSubject(subject);
+ message.setText(content);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ message.setCc(cc);
+ }
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送HTML邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ @Override
+ public void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException {
+ MimeMessage message = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(content, true);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ helper.setCc(cc);
+ }
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送带附件的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param filePath 附件地址
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ @Override
+ public void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc) throws MessagingException {
+ MimeMessage message = mailSender.createMimeMessage();
+
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(content, true);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ helper.setCc(cc);
+ }
+ FileSystemResource file = new FileSystemResource(new File(filePath));
+ String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
+ helper.addAttachment(fileName, file);
+
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送正文中有静态资源的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param rscPath 静态资源地址
+ * @param rscId 静态资源id
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ @Override
+ public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc) throws MessagingException {
+ MimeMessage message = mailSender.createMimeMessage();
+
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(content, true);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ helper.setCc(cc);
+ }
+ FileSystemResource res = new FileSystemResource(new File(rscPath));
+ helper.addInline(rscId, res);
+
+ mailSender.send(message);
+ }
+}
+```
+
+## MailServiceTest.java
+
+```java
+/**
+ *
+ * 邮件测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-21 13:49
+ */
+public class MailServiceTest extends SpringBootDemoEmailApplicationTests {
+ @Autowired
+ private MailService mailService;
+ @Autowired
+ private TemplateEngine templateEngine;
+ @Autowired
+ private ApplicationContext context;
+
+ /**
+ * 测试简单邮件
+ */
+ @Test
+ public void sendSimpleMail() {
+ mailService.sendSimpleMail("237497819@qq.com", "这是一封简单邮件", "这是一封普通的SpringBoot测试邮件");
+ }
+
+ /**
+ * 测试HTML邮件
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendHtmlMail() throws MessagingException {
+ Context context = new Context();
+ context.setVariable("project", "Spring Boot Demo");
+ context.setVariable("author", "Yangkai.Shen");
+ context.setVariable("url", "https://github.com/xkcoding/spring-boot-demo");
+
+ String emailTemplate = templateEngine.process("welcome", context);
+ mailService.sendHtmlMail("237497819@qq.com", "这是一封模板HTML邮件", emailTemplate);
+ }
+
+ /**
+ * 测试HTML邮件,自定义模板目录
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendHtmlMail2() throws MessagingException {
+
+ SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
+ templateResolver.setApplicationContext(context);
+ templateResolver.setCacheable(false);
+ templateResolver.setPrefix("classpath:/email/");
+ templateResolver.setSuffix(".html");
+
+ templateEngine.setTemplateResolver(templateResolver);
+
+ Context context = new Context();
+ context.setVariable("project", "Spring Boot Demo");
+ context.setVariable("author", "Yangkai.Shen");
+ context.setVariable("url", "https://github.com/xkcoding/spring-boot-demo");
+
+ String emailTemplate = templateEngine.process("test", context);
+ mailService.sendHtmlMail("237497819@qq.com", "这是一封模板HTML邮件", emailTemplate);
+ }
+
+ /**
+ * 测试附件邮件
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendAttachmentsMail() throws MessagingException {
+ URL resource = ResourceUtil.getResource("static/xkcoding.png");
+ mailService.sendAttachmentsMail("237497819@qq.com", "这是一封带附件的邮件", "邮件中有附件,请注意查收!", resource.getPath());
+ }
+
+ /**
+ * 测试静态资源邮件
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendResourceMail() throws MessagingException {
+ String rscId = "xkcoding";
+ String content = "这是带静态资源的邮件 ";
+ URL resource = ResourceUtil.getResource("static/xkcoding.png");
+ mailService.sendResourceMail("237497819@qq.com", "这是一封带静态资源的邮件", content, resource.getPath(), rscId);
+ }
+}
+```
+
+## welcome.html
+
+> 此文件为邮件模板,位于 resources/templates 目录下
+
+```html
+
+
+
+
+ SpringBootDemo(入门SpringBoot的首选Demo)
+
+
+
+
+
欢迎使用 - Powered By
+
+
+
+ 如果对你有帮助,请任意打赏
+
+
+
+
+
+
+
+
+
微信打赏
+
+
+
+
+
支付宝打赏
+
+
+
+
+
+
+
+
+```
+
+## 参考
+
+- Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-email
+- Spring Boot 官方文档:https://docs.spring.io/spring/docs/5.1.2.RELEASE/spring-framework-reference/integration.html#mail
diff --git a/demo-email/pom.xml b/demo-email/pom.xml
new file mode 100644
index 0000000..2c7b9f4
--- /dev/null
+++ b/demo-email/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+
+ demo-email
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-email
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 2.1.1
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+
+ com.github.ulisesbocchio
+ jasypt-spring-boot-starter
+ ${jasypt.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+
+ demo-email
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-email/src/main/java/com/xkcoding/email/SpringBootDemoEmailApplication.java b/demo-email/src/main/java/com/xkcoding/email/SpringBootDemoEmailApplication.java
new file mode 100644
index 0000000..9e054fa
--- /dev/null
+++ b/demo-email/src/main/java/com/xkcoding/email/SpringBootDemoEmailApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.email;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-04 22:38
+ */
+@SpringBootApplication
+public class SpringBootDemoEmailApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoEmailApplication.class, args);
+ }
+}
diff --git a/demo-email/src/main/java/com/xkcoding/email/service/MailService.java b/demo-email/src/main/java/com/xkcoding/email/service/MailService.java
new file mode 100644
index 0000000..b7e5764
--- /dev/null
+++ b/demo-email/src/main/java/com/xkcoding/email/service/MailService.java
@@ -0,0 +1,60 @@
+package com.xkcoding.email.service;
+
+import javax.mail.MessagingException;
+
+/**
+ *
+ * 邮件接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-21 11:16
+ */
+public interface MailService {
+ /**
+ * 发送文本邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ */
+ void sendSimpleMail(String to, String subject, String content, String... cc);
+
+ /**
+ * 发送HTML邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException;
+
+ /**
+ * 发送带附件的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param filePath 附件地址
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc) throws MessagingException;
+
+ /**
+ * 发送正文中有静态资源的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param rscPath 静态资源地址
+ * @param rscId 静态资源id
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc) throws MessagingException;
+
+}
diff --git a/demo-email/src/main/java/com/xkcoding/email/service/impl/MailServiceImpl.java b/demo-email/src/main/java/com/xkcoding/email/service/impl/MailServiceImpl.java
new file mode 100644
index 0000000..59a8e13
--- /dev/null
+++ b/demo-email/src/main/java/com/xkcoding/email/service/impl/MailServiceImpl.java
@@ -0,0 +1,133 @@
+package com.xkcoding.email.service.impl;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.xkcoding.email.service.MailService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.io.File;
+
+/**
+ *
+ * 邮件接口
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-21 13:49
+ */
+@Service
+public class MailServiceImpl implements MailService {
+ @Autowired
+ private JavaMailSender mailSender;
+ @Value("${spring.mail.username}")
+ private String from;
+
+ /**
+ * 发送文本邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ */
+ @Override
+ public void sendSimpleMail(String to, String subject, String content, String... cc) {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(from);
+ message.setTo(to);
+ message.setSubject(subject);
+ message.setText(content);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ message.setCc(cc);
+ }
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送HTML邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ @Override
+ public void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException {
+ MimeMessage message = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(content, true);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ helper.setCc(cc);
+ }
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送带附件的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param filePath 附件地址
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ @Override
+ public void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc) throws MessagingException {
+ MimeMessage message = mailSender.createMimeMessage();
+
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(content, true);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ helper.setCc(cc);
+ }
+ FileSystemResource file = new FileSystemResource(new File(filePath));
+ String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
+ helper.addAttachment(fileName, file);
+
+ mailSender.send(message);
+ }
+
+ /**
+ * 发送正文中有静态资源的邮件
+ *
+ * @param to 收件人地址
+ * @param subject 邮件主题
+ * @param content 邮件内容
+ * @param rscPath 静态资源地址
+ * @param rscId 静态资源id
+ * @param cc 抄送地址
+ * @throws MessagingException 邮件发送异常
+ */
+ @Override
+ public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc) throws MessagingException {
+ MimeMessage message = mailSender.createMimeMessage();
+
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(content, true);
+ if (ArrayUtil.isNotEmpty(cc)) {
+ helper.setCc(cc);
+ }
+ FileSystemResource res = new FileSystemResource(new File(rscPath));
+ helper.addInline(rscId, res);
+
+ mailSender.send(message);
+ }
+}
diff --git a/spring-boot-demo-email/src/main/resources/application.yml b/demo-email/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-email/src/main/resources/application.yml
rename to demo-email/src/main/resources/application.yml
diff --git a/spring-boot-demo-email/src/main/resources/email/test.html b/demo-email/src/main/resources/email/test.html
similarity index 100%
rename from spring-boot-demo-email/src/main/resources/email/test.html
rename to demo-email/src/main/resources/email/test.html
diff --git a/spring-boot-demo-email/src/main/resources/static/xkcoding.png b/demo-email/src/main/resources/static/xkcoding.png
similarity index 100%
rename from spring-boot-demo-email/src/main/resources/static/xkcoding.png
rename to demo-email/src/main/resources/static/xkcoding.png
diff --git a/spring-boot-demo-email/src/main/resources/templates/welcome.html b/demo-email/src/main/resources/templates/welcome.html
similarity index 100%
rename from spring-boot-demo-email/src/main/resources/templates/welcome.html
rename to demo-email/src/main/resources/templates/welcome.html
diff --git a/demo-email/src/test/java/com/xkcoding/email/PasswordTest.java b/demo-email/src/test/java/com/xkcoding/email/PasswordTest.java
new file mode 100644
index 0000000..3f119e9
--- /dev/null
+++ b/demo-email/src/test/java/com/xkcoding/email/PasswordTest.java
@@ -0,0 +1,34 @@
+package com.xkcoding.email;
+
+import org.jasypt.encryption.StringEncryptor;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ *
+ * 数据库密码测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-08-27 16:15
+ */
+public class PasswordTest extends SpringBootDemoEmailApplicationTests {
+ @Autowired
+ private StringEncryptor encryptor;
+
+ /**
+ * 生成加密密码
+ */
+ @Test
+ public void testGeneratePassword() {
+ // 你的邮箱密码
+ String password = "Just4Test!";
+ // 加密后的密码(注意:配置上去的时候需要加 ENC(加密密码))
+ String encryptPassword = encryptor.encrypt(password);
+ String decryptPassword = encryptor.decrypt(encryptPassword);
+
+ System.out.println("password = " + password);
+ System.out.println("encryptPassword = " + encryptPassword);
+ System.out.println("decryptPassword = " + decryptPassword);
+ }
+}
diff --git a/demo-email/src/test/java/com/xkcoding/email/SpringBootDemoEmailApplicationTests.java b/demo-email/src/test/java/com/xkcoding/email/SpringBootDemoEmailApplicationTests.java
new file mode 100644
index 0000000..2b74a39
--- /dev/null
+++ b/demo-email/src/test/java/com/xkcoding/email/SpringBootDemoEmailApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.email;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoEmailApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/demo-email/src/test/java/com/xkcoding/email/service/MailServiceTest.java b/demo-email/src/test/java/com/xkcoding/email/service/MailServiceTest.java
new file mode 100644
index 0000000..b23d352
--- /dev/null
+++ b/demo-email/src/test/java/com/xkcoding/email/service/MailServiceTest.java
@@ -0,0 +1,103 @@
+package com.xkcoding.email.service;
+
+import cn.hutool.core.io.resource.ResourceUtil;
+import com.xkcoding.email.SpringBootDemoEmailApplicationTests;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
+
+import javax.mail.MessagingException;
+import java.net.URL;
+
+/**
+ *
+ * 邮件测试
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-11-21 13:49
+ */
+public class MailServiceTest extends SpringBootDemoEmailApplicationTests {
+ @Autowired
+ private MailService mailService;
+ @Autowired
+ private TemplateEngine templateEngine;
+ @Autowired
+ private ApplicationContext context;
+
+ /**
+ * 测试简单邮件
+ */
+ @Test
+ public void sendSimpleMail() {
+ mailService.sendSimpleMail("237497819@qq.com", "这是一封简单邮件", "这是一封普通的SpringBoot测试邮件");
+ }
+
+ /**
+ * 测试HTML邮件
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendHtmlMail() throws MessagingException {
+ Context context = new Context();
+ context.setVariable("project", "Spring Boot Demo");
+ context.setVariable("author", "Yangkai.Shen");
+ context.setVariable("url", "https://github.com/xkcoding/spring-boot-demo");
+
+ String emailTemplate = templateEngine.process("welcome", context);
+ mailService.sendHtmlMail("237497819@qq.com", "这是一封模板HTML邮件", emailTemplate);
+ }
+
+ /**
+ * 测试HTML邮件,自定义模板目录
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendHtmlMail2() throws MessagingException {
+
+ SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
+ templateResolver.setApplicationContext(context);
+ templateResolver.setCacheable(false);
+ templateResolver.setPrefix("classpath:/email/");
+ templateResolver.setSuffix(".html");
+
+ templateEngine.setTemplateResolver(templateResolver);
+
+ Context context = new Context();
+ context.setVariable("project", "Spring Boot Demo");
+ context.setVariable("author", "Yangkai.Shen");
+ context.setVariable("url", "https://github.com/xkcoding/spring-boot-demo");
+
+ String emailTemplate = templateEngine.process("test", context);
+ mailService.sendHtmlMail("237497819@qq.com", "这是一封模板HTML邮件", emailTemplate);
+ }
+
+ /**
+ * 测试附件邮件
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendAttachmentsMail() throws MessagingException {
+ URL resource = ResourceUtil.getResource("static/xkcoding.png");
+ mailService.sendAttachmentsMail("237497819@qq.com", "这是一封带附件的邮件", "邮件中有附件,请注意查收!", resource.getPath());
+ }
+
+ /**
+ * 测试静态资源邮件
+ *
+ * @throws MessagingException 邮件异常
+ */
+ @Test
+ public void sendResourceMail() throws MessagingException {
+ String rscId = "xkcoding";
+ String content = "这是带静态资源的邮件 ";
+ URL resource = ResourceUtil.getResource("static/xkcoding.png");
+ mailService.sendResourceMail("237497819@qq.com", "这是一封带静态资源的邮件", content, resource.getPath(), rscId);
+ }
+}
diff --git a/spring-boot-demo-exception-handler/.gitignore b/demo-exception-handler/.gitignore
similarity index 100%
rename from spring-boot-demo-exception-handler/.gitignore
rename to demo-exception-handler/.gitignore
diff --git a/demo-exception-handler/README.md b/demo-exception-handler/README.md
new file mode 100644
index 0000000..040276f
--- /dev/null
+++ b/demo-exception-handler/README.md
@@ -0,0 +1,260 @@
+# spring-boot-demo-exception-handler
+
+> 此 demo 演示了如何在Spring Boot中进行统一的异常处理,包括了两种方式的处理:第一种对常见API形式的接口进行异常处理,统一封装返回格式;第二种是对模板页面请求的异常处理,统一处理错误页面。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-exception-handler
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-exception-handler
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-exception-handler
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## ApiResponse.java
+
+> 统一的API格式返回封装,里面涉及到的 `BaseException` 和`Status` 这两个类,具体代码见 demo。
+
+```java
+/**
+ *
+ * 通用的 API 接口封装
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 20:57
+ */
+@Data
+public class ApiResponse {
+ /**
+ * 状态码
+ */
+ private Integer code;
+
+ /**
+ * 返回内容
+ */
+ private String message;
+
+ /**
+ * 返回数据
+ */
+ private Object data;
+
+ /**
+ * 无参构造函数
+ */
+ private ApiResponse() {
+
+ }
+
+ /**
+ * 全参构造函数
+ *
+ * @param code 状态码
+ * @param message 返回内容
+ * @param data 返回数据
+ */
+ private ApiResponse(Integer code, String message, Object data) {
+ this.code = code;
+ this.message = message;
+ this.data = data;
+ }
+
+ /**
+ * 构造一个自定义的API返回
+ *
+ * @param code 状态码
+ * @param message 返回内容
+ * @param data 返回数据
+ * @return ApiResponse
+ */
+ public static ApiResponse of(Integer code, String message, Object data) {
+ return new ApiResponse(code, message, data);
+ }
+
+ /**
+ * 构造一个成功且带数据的API返回
+ *
+ * @param data 返回数据
+ * @return ApiResponse
+ */
+ public static ApiResponse ofSuccess(Object data) {
+ return ofStatus(Status.OK, data);
+ }
+
+ /**
+ * 构造一个成功且自定义消息的API返回
+ *
+ * @param message 返回内容
+ * @return ApiResponse
+ */
+ public static ApiResponse ofMessage(String message) {
+ return of(Status.OK.getCode(), message, null);
+ }
+
+ /**
+ * 构造一个有状态的API返回
+ *
+ * @param status 状态 {@link Status}
+ * @return ApiResponse
+ */
+ public static ApiResponse ofStatus(Status status) {
+ return ofStatus(status, null);
+ }
+
+ /**
+ * 构造一个有状态且带数据的API返回
+ *
+ * @param status 状态 {@link Status}
+ * @param data 返回数据
+ * @return ApiResponse
+ */
+ public static ApiResponse ofStatus(Status status, Object data) {
+ return of(status.getCode(), status.getMessage(), data);
+ }
+
+ /**
+ * 构造一个异常且带数据的API返回
+ *
+ * @param t 异常
+ * @param data 返回数据
+ * @param {@link BaseException} 的子类
+ * @return ApiResponse
+ */
+ public static ApiResponse ofException(T t, Object data) {
+ return of(t.getCode(), t.getMessage(), data);
+ }
+
+ /**
+ * 构造一个异常且带数据的API返回
+ *
+ * @param t 异常
+ * @param {@link BaseException} 的子类
+ * @return ApiResponse
+ */
+ public static ApiResponse ofException(T t) {
+ return ofException(t, null);
+ }
+}
+```
+
+## DemoExceptionHandler.java
+
+```java
+/**
+ *
+ * 统一异常处理
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 21:26
+ */
+@ControllerAdvice
+@Slf4j
+public class DemoExceptionHandler {
+ private static final String DEFAULT_ERROR_VIEW = "error";
+
+ /**
+ * 统一 json 异常处理
+ *
+ * @param exception JsonException
+ * @return 统一返回 json 格式
+ */
+ @ExceptionHandler(value = JsonException.class)
+ @ResponseBody
+ public ApiResponse jsonErrorHandler(JsonException exception) {
+ log.error("【JsonException】:{}", exception.getMessage());
+ return ApiResponse.ofException(exception);
+ }
+
+ /**
+ * 统一 页面 异常处理
+ *
+ * @param exception PageException
+ * @return 统一跳转到异常页面
+ */
+ @ExceptionHandler(value = PageException.class)
+ public ModelAndView pageErrorHandler(PageException exception) {
+ log.error("【DemoPageException】:{}", exception.getMessage());
+ ModelAndView view = new ModelAndView();
+ view.addObject("message", exception.getMessage());
+ view.setViewName(DEFAULT_ERROR_VIEW);
+ return view;
+ }
+}
+```
+
+## error.html
+
+> 位于 `src/main/resources/template` 目录下
+
+```html
+
+
+
+
+ 统一页面异常处理
+
+
+统一页面异常处理
+
+
+
+```
+
diff --git a/demo-exception-handler/pom.xml b/demo-exception-handler/pom.xml
new file mode 100644
index 0000000..7e543f1
--- /dev/null
+++ b/demo-exception-handler/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ demo-exception-handler
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-exception-handler
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-exception-handler
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplication.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplication.java
new file mode 100644
index 0000000..68c50c0
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.exception.handler;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 20:49
+ */
+@SpringBootApplication
+public class SpringBootDemoExceptionHandlerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoExceptionHandlerApplication.class, args);
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/constant/Status.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/constant/Status.java
new file mode 100644
index 0000000..3f0eb18
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/constant/Status.java
@@ -0,0 +1,37 @@
+package com.xkcoding.exception.handler.constant;
+
+import lombok.Getter;
+
+/**
+ *
+ * 状态码封装
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 21:02
+ */
+@Getter
+public enum Status {
+ /**
+ * 操作成功
+ */
+ OK(200, "操作成功"),
+
+ /**
+ * 未知异常
+ */
+ UNKNOWN_ERROR(500, "服务器出错啦");
+ /**
+ * 状态码
+ */
+ private Integer code;
+ /**
+ * 内容
+ */
+ private String message;
+
+ Status(Integer code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/controller/TestController.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/controller/TestController.java
new file mode 100644
index 0000000..493e93c
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/controller/TestController.java
@@ -0,0 +1,33 @@
+package com.xkcoding.exception.handler.controller;
+
+import com.xkcoding.exception.handler.constant.Status;
+import com.xkcoding.exception.handler.exception.JsonException;
+import com.xkcoding.exception.handler.exception.PageException;
+import com.xkcoding.exception.handler.model.ApiResponse;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ *
+ * 测试Controller
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 20:49
+ */
+@Controller
+public class TestController {
+
+ @GetMapping("/json")
+ @ResponseBody
+ public ApiResponse jsonException() {
+ throw new JsonException(Status.UNKNOWN_ERROR);
+ }
+
+ @GetMapping("/page")
+ public ModelAndView pageException() {
+ throw new PageException(Status.UNKNOWN_ERROR);
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/BaseException.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/BaseException.java
new file mode 100644
index 0000000..2d42003
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/BaseException.java
@@ -0,0 +1,32 @@
+package com.xkcoding.exception.handler.exception;
+
+import com.xkcoding.exception.handler.constant.Status;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ *
+ * 异常基类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 21:31
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class BaseException extends RuntimeException {
+ private Integer code;
+ private String message;
+
+ public BaseException(Status status) {
+ super(status.getMessage());
+ this.code = status.getCode();
+ this.message = status.getMessage();
+ }
+
+ public BaseException(Integer code, String message) {
+ super(message);
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/JsonException.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/JsonException.java
new file mode 100644
index 0000000..fb71770
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/JsonException.java
@@ -0,0 +1,24 @@
+package com.xkcoding.exception.handler.exception;
+
+import com.xkcoding.exception.handler.constant.Status;
+import lombok.Getter;
+
+/**
+ *
+ * JSON异常
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 21:18
+ */
+@Getter
+public class JsonException extends BaseException {
+
+ public JsonException(Status status) {
+ super(status);
+ }
+
+ public JsonException(Integer code, String message) {
+ super(code, message);
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/PageException.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/PageException.java
new file mode 100644
index 0000000..97c9ba7
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/PageException.java
@@ -0,0 +1,24 @@
+package com.xkcoding.exception.handler.exception;
+
+import com.xkcoding.exception.handler.constant.Status;
+import lombok.Getter;
+
+/**
+ *
+ * 页面异常
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 21:18
+ */
+@Getter
+public class PageException extends BaseException {
+
+ public PageException(Status status) {
+ super(status);
+ }
+
+ public PageException(Integer code, String message) {
+ super(code, message);
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/handler/DemoExceptionHandler.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/handler/DemoExceptionHandler.java
new file mode 100644
index 0000000..32eacf8
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/handler/DemoExceptionHandler.java
@@ -0,0 +1,52 @@
+package com.xkcoding.exception.handler.handler;
+
+import com.xkcoding.exception.handler.exception.JsonException;
+import com.xkcoding.exception.handler.exception.PageException;
+import com.xkcoding.exception.handler.model.ApiResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ *
+ * 统一异常处理
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 21:26
+ */
+@ControllerAdvice
+@Slf4j
+public class DemoExceptionHandler {
+ private static final String DEFAULT_ERROR_VIEW = "error";
+
+ /**
+ * 统一 json 异常处理
+ *
+ * @param exception JsonException
+ * @return 统一返回 json 格式
+ */
+ @ExceptionHandler(value = JsonException.class)
+ @ResponseBody
+ public ApiResponse jsonErrorHandler(JsonException exception) {
+ log.error("【JsonException】:{}", exception.getMessage());
+ return ApiResponse.ofException(exception);
+ }
+
+ /**
+ * 统一 页面 异常处理
+ *
+ * @param exception PageException
+ * @return 统一跳转到异常页面
+ */
+ @ExceptionHandler(value = PageException.class)
+ public ModelAndView pageErrorHandler(PageException exception) {
+ log.error("【DemoPageException】:{}", exception.getMessage());
+ ModelAndView view = new ModelAndView();
+ view.addObject("message", exception.getMessage());
+ view.setViewName(DEFAULT_ERROR_VIEW);
+ return view;
+ }
+}
diff --git a/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/model/ApiResponse.java b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/model/ApiResponse.java
new file mode 100644
index 0000000..8c5fa71
--- /dev/null
+++ b/demo-exception-handler/src/main/java/com/xkcoding/exception/handler/model/ApiResponse.java
@@ -0,0 +1,127 @@
+package com.xkcoding.exception.handler.model;
+
+import com.xkcoding.exception.handler.constant.Status;
+import com.xkcoding.exception.handler.exception.BaseException;
+import lombok.Data;
+
+/**
+ *
+ * 通用的 API 接口封装
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-02 20:57
+ */
+@Data
+public class ApiResponse {
+ /**
+ * 状态码
+ */
+ private Integer code;
+
+ /**
+ * 返回内容
+ */
+ private String message;
+
+ /**
+ * 返回数据
+ */
+ private Object data;
+
+ /**
+ * 无参构造函数
+ */
+ private ApiResponse() {
+
+ }
+
+ /**
+ * 全参构造函数
+ *
+ * @param code 状态码
+ * @param message 返回内容
+ * @param data 返回数据
+ */
+ private ApiResponse(Integer code, String message, Object data) {
+ this.code = code;
+ this.message = message;
+ this.data = data;
+ }
+
+ /**
+ * 构造一个自定义的API返回
+ *
+ * @param code 状态码
+ * @param message 返回内容
+ * @param data 返回数据
+ * @return ApiResponse
+ */
+ public static ApiResponse of(Integer code, String message, Object data) {
+ return new ApiResponse(code, message, data);
+ }
+
+ /**
+ * 构造一个成功且带数据的API返回
+ *
+ * @param data 返回数据
+ * @return ApiResponse
+ */
+ public static ApiResponse ofSuccess(Object data) {
+ return ofStatus(Status.OK, data);
+ }
+
+ /**
+ * 构造一个成功且自定义消息的API返回
+ *
+ * @param message 返回内容
+ * @return ApiResponse
+ */
+ public static ApiResponse ofMessage(String message) {
+ return of(Status.OK.getCode(), message, null);
+ }
+
+ /**
+ * 构造一个有状态的API返回
+ *
+ * @param status 状态 {@link Status}
+ * @return ApiResponse
+ */
+ public static ApiResponse ofStatus(Status status) {
+ return ofStatus(status, null);
+ }
+
+ /**
+ * 构造一个有状态且带数据的API返回
+ *
+ * @param status 状态 {@link Status}
+ * @param data 返回数据
+ * @return ApiResponse
+ */
+ public static ApiResponse ofStatus(Status status, Object data) {
+ return of(status.getCode(), status.getMessage(), data);
+ }
+
+ /**
+ * 构造一个异常且带数据的API返回
+ *
+ * @param t 异常
+ * @param data 返回数据
+ * @param {@link BaseException} 的子类
+ * @return ApiResponse
+ */
+ public static ApiResponse ofException(T t, Object data) {
+ return of(t.getCode(), t.getMessage(), data);
+ }
+
+ /**
+ * 构造一个异常且带数据的API返回
+ *
+ * @param t 异常
+ * @param {@link BaseException} 的子类
+ * @return ApiResponse
+ */
+ public static ApiResponse ofException(T t) {
+ return ofException(t, null);
+ }
+}
diff --git a/spring-boot-demo-exception-handler/src/main/resources/application.yml b/demo-exception-handler/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-exception-handler/src/main/resources/application.yml
rename to demo-exception-handler/src/main/resources/application.yml
diff --git a/spring-boot-demo-exception-handler/src/main/resources/templates/error.html b/demo-exception-handler/src/main/resources/templates/error.html
similarity index 100%
rename from spring-boot-demo-exception-handler/src/main/resources/templates/error.html
rename to demo-exception-handler/src/main/resources/templates/error.html
diff --git a/demo-exception-handler/src/test/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplicationTests.java b/demo-exception-handler/src/test/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplicationTests.java
new file mode 100644
index 0000000..489e1b2
--- /dev/null
+++ b/demo-exception-handler/src/test/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.exception.handler;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoExceptionHandlerApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/spring-boot-demo-flyway/.gitignore b/demo-flyway/.gitignore
similarity index 100%
rename from spring-boot-demo-flyway/.gitignore
rename to demo-flyway/.gitignore
diff --git a/spring-boot-demo-flyway/README.md b/demo-flyway/README.md
similarity index 100%
rename from spring-boot-demo-flyway/README.md
rename to demo-flyway/README.md
diff --git a/demo-flyway/pom.xml b/demo-flyway/pom.xml
new file mode 100644
index 0000000..37cf701
--- /dev/null
+++ b/demo-flyway/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+ demo-flyway
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-flyway
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.flywaydb
+ flyway-core
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+
+
+ demo-flyway
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-flyway/src/main/java/com/xkcoding/flyway/SpringBootDemoFlywayApplication.java b/demo-flyway/src/main/java/com/xkcoding/flyway/SpringBootDemoFlywayApplication.java
new file mode 100644
index 0000000..abc35f9
--- /dev/null
+++ b/demo-flyway/src/main/java/com/xkcoding/flyway/SpringBootDemoFlywayApplication.java
@@ -0,0 +1,21 @@
+package com.xkcoding.flyway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2020-03-04 18:30
+ */
+@SpringBootApplication
+public class SpringBootDemoFlywayApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoFlywayApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-demo-flyway/src/main/resources/application.yml b/demo-flyway/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-flyway/src/main/resources/application.yml
rename to demo-flyway/src/main/resources/application.yml
diff --git a/spring-boot-demo-flyway/src/main/resources/db/migration/V1_0__INIT.sql b/demo-flyway/src/main/resources/db/migration/V1_0__INIT.sql
similarity index 100%
rename from spring-boot-demo-flyway/src/main/resources/db/migration/V1_0__INIT.sql
rename to demo-flyway/src/main/resources/db/migration/V1_0__INIT.sql
diff --git a/spring-boot-demo-flyway/src/main/resources/db/migration/V1_1__ALTER.sql b/demo-flyway/src/main/resources/db/migration/V1_1__ALTER.sql
similarity index 100%
rename from spring-boot-demo-flyway/src/main/resources/db/migration/V1_1__ALTER.sql
rename to demo-flyway/src/main/resources/db/migration/V1_1__ALTER.sql
diff --git a/demo-flyway/src/test/java/com/xkcoding/AppTest.java b/demo-flyway/src/test/java/com/xkcoding/AppTest.java
new file mode 100644
index 0000000..a6bfab6
--- /dev/null
+++ b/demo-flyway/src/test/java/com/xkcoding/AppTest.java
@@ -0,0 +1,18 @@
+package com.xkcoding;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit test for simple App.
+ */
+public class AppTest {
+ /**
+ * Rigorous Test :-)
+ */
+ @Test
+ public void shouldAnswerWithTrue() {
+ assertTrue(true);
+ }
+}
diff --git a/spring-boot-demo-graylog/.gitignore b/demo-graylog/.gitignore
similarity index 100%
rename from spring-boot-demo-graylog/.gitignore
rename to demo-graylog/.gitignore
diff --git a/spring-boot-demo-graylog/README.md b/demo-graylog/README.md
similarity index 100%
rename from spring-boot-demo-graylog/README.md
rename to demo-graylog/README.md
diff --git a/demo-graylog/pom.xml b/demo-graylog/pom.xml
new file mode 100644
index 0000000..a27e632
--- /dev/null
+++ b/demo-graylog/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+ demo-graylog
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-graylog
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ de.siegmar
+ logback-gelf
+ 2.0.0
+
+
+
+
+ demo-graylog
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-graylog/src/main/java/com/xkcoding/graylog/SpringBootDemoGraylogApplication.java b/demo-graylog/src/main/java/com/xkcoding/graylog/SpringBootDemoGraylogApplication.java
new file mode 100644
index 0000000..efb57c8
--- /dev/null
+++ b/demo-graylog/src/main/java/com/xkcoding/graylog/SpringBootDemoGraylogApplication.java
@@ -0,0 +1,21 @@
+package com.xkcoding.graylog;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-04-23 09:43
+ */
+@SpringBootApplication
+public class SpringBootDemoGraylogApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoGraylogApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-demo-graylog/src/main/resources/application.yml b/demo-graylog/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-graylog/src/main/resources/application.yml
rename to demo-graylog/src/main/resources/application.yml
diff --git a/spring-boot-demo-graylog/src/main/resources/logback-spring.xml b/demo-graylog/src/main/resources/logback-spring.xml
similarity index 100%
rename from spring-boot-demo-graylog/src/main/resources/logback-spring.xml
rename to demo-graylog/src/main/resources/logback-spring.xml
diff --git a/spring-boot-demo-graylog/src/test/java/com/xkcoding/graylog/SpringBootDemoGraylogApplicationTests.java b/demo-graylog/src/test/java/com/xkcoding/graylog/SpringBootDemoGraylogApplicationTests.java
similarity index 100%
rename from spring-boot-demo-graylog/src/test/java/com/xkcoding/graylog/SpringBootDemoGraylogApplicationTests.java
rename to demo-graylog/src/test/java/com/xkcoding/graylog/SpringBootDemoGraylogApplicationTests.java
diff --git a/spring-boot-demo-helloworld/.gitignore b/demo-helloworld/.gitignore
similarity index 100%
rename from spring-boot-demo-helloworld/.gitignore
rename to demo-helloworld/.gitignore
diff --git a/demo-helloworld/README.md b/demo-helloworld/README.md
new file mode 100644
index 0000000..d0c5cd8
--- /dev/null
+++ b/demo-helloworld/README.md
@@ -0,0 +1,111 @@
+# spring-boot-demo-helloworld
+
+## Runing spring boot demo helloworld
+
+```sh
+ $ mvn spring-boot:run
+```
+##
+> 本 demo 演示如何使用 Spring Boot 写一个hello world
+
+### pom.xml
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-helloworld
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-helloworld
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ spring-boot-demo-helloworld
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+### SpringBootDemoHelloworldApplication.java
+
+```java
+/**
+ *
+ * SpringBoot启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-09-28 14:49
+ */
+@SpringBootApplication
+@RestController
+public class SpringBootDemoHelloworldApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoHelloworldApplication.class, args);
+ }
+
+ /**
+ * Hello,World
+ *
+ * @param who 参数,非必须
+ * @return Hello, ${who}
+ */
+ @GetMapping("/hello")
+ public String sayHello(@RequestParam(required = false, name = "who") String who) {
+ if (StrUtil.isBlank(who)) {
+ who = "World";
+ }
+ return StrUtil.format("Hello, {}!", who);
+ }
+}
+```
+
+### application.yml
+
+```yaml
+server:
+ port: 8080
+ servlet:
+ context-path: /demo
+```
+
diff --git a/demo-helloworld/pom.xml b/demo-helloworld/pom.xml
new file mode 100644
index 0000000..25ec2d9
--- /dev/null
+++ b/demo-helloworld/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ demo-helloworld
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-helloworld
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ demo-helloworld
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-helloworld/src/main/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplication.java b/demo-helloworld/src/main/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplication.java
new file mode 100644
index 0000000..55b3be6
--- /dev/null
+++ b/demo-helloworld/src/main/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplication.java
@@ -0,0 +1,39 @@
+package com.xkcoding.helloworld;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * SpringBoot启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-09-28 14:49
+ */
+@SpringBootApplication
+@RestController
+public class SpringBootDemoHelloworldApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoHelloworldApplication.class, args);
+ }
+
+ /**
+ * Hello,World
+ *
+ * @param who 参数,非必须
+ * @return Hello, ${who}
+ */
+ @GetMapping("/hello")
+ public String sayHello(@RequestParam(required = false, name = "who") String who) {
+ if (StrUtil.isBlank(who)) {
+ who = "World";
+ }
+ return StrUtil.format("Hello, {}!", who);
+ }
+}
diff --git a/spring-boot-demo-helloworld/src/main/resources/application.yml b/demo-helloworld/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-helloworld/src/main/resources/application.yml
rename to demo-helloworld/src/main/resources/application.yml
diff --git a/demo-helloworld/src/test/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplicationTests.java b/demo-helloworld/src/test/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplicationTests.java
new file mode 100644
index 0000000..547e54b
--- /dev/null
+++ b/demo-helloworld/src/test/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.helloworld;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoHelloworldApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/spring-boot-demo-https/.gitignore b/demo-https/.gitignore
similarity index 100%
rename from spring-boot-demo-https/.gitignore
rename to demo-https/.gitignore
diff --git a/demo-https/README.md b/demo-https/README.md
new file mode 100644
index 0000000..042cb6f
--- /dev/null
+++ b/demo-https/README.md
@@ -0,0 +1,110 @@
+# spring-boot-demo-https
+
+> 此 demo 主要演示了 Spring Boot 如何集成 https
+
+## 1. 生成证书
+
+首先使用 jdk 自带的 keytool 命令生成证书复制到项目的 `resources` 目录下(生成的证书一般在用户目录下 C:\Users\Administrator\server.keystore)
+
+> 自己生成的证书浏览器会有危险提示,去ssl网站上使用金钱申请则不会
+
+![ssl 命令截图](ssl.png)
+
+## 2. 添加配置
+
+1. 在配置文件配置生成的证书
+
+```yaml
+server:
+ ssl:
+ # 证书路径
+ key-store: classpath:server.keystore
+ key-alias: tomcat
+ enabled: true
+ key-store-type: JKS
+ #与申请时输入一致
+ key-store-password: 123456
+ # 浏览器默认端口 和 80 类似
+ port: 443
+```
+
+2. 配置 Tomcat
+
+```java
+/**
+ *
+ * HTTPS 配置类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2020-01-19 10:31
+ */
+@Configuration
+public class HttpsConfig {
+ /**
+ * 配置 http(80) -> 强制跳转到 https(443)
+ */
+ @Bean
+ public Connector connector() {
+ Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+ connector.setScheme("http");
+ connector.setPort(80);
+ connector.setSecure(false);
+ connector.setRedirectPort(443);
+ return connector;
+ }
+
+ @Bean
+ public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector) {
+ TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
+ @Override
+ protected void postProcessContext(Context context) {
+ SecurityConstraint securityConstraint = new SecurityConstraint();
+ securityConstraint.setUserConstraint("CONFIDENTIAL");
+ SecurityCollection collection = new SecurityCollection();
+ collection.addPattern("/*");
+ securityConstraint.addCollection(collection);
+ context.addConstraint(securityConstraint);
+ }
+ };
+ tomcat.addAdditionalTomcatConnectors(connector);
+ return tomcat;
+ }
+}
+```
+
+## 3. 测试
+
+启动项目,浏览器访问 http://localhost 将自动跳转到 https://localhost
+
+## 4. 参考
+
+- `keytool`命令参考
+
+```bash
+$ keytool --help
+密钥和证书管理工具
+
+命令:
+
+ -certreq 生成证书请求
+ -changealias 更改条目的别名
+ -delete 删除条目
+ -exportcert 导出证书
+ -genkeypair 生成密钥对
+ -genseckey 生成密钥
+ -gencert 根据证书请求生成证书
+ -importcert 导入证书或证书链
+ -importpass 导入口令
+ -importkeystore 从其他密钥库导入一个或所有条目
+ -keypasswd 更改条目的密钥口令
+ -list 列出密钥库中的条目
+ -printcert 打印证书内容
+ -printcertreq 打印证书请求的内容
+ -printcrl 打印 CRL 文件的内容
+ -storepasswd 更改密钥库的存储口令
+
+使用 "keytool -command_name -help" 获取 command_name 的用法
+```
+
+- [Java Keytool工具简介](https://blog.csdn.net/liumiaocn/article/details/61921014)
diff --git a/demo-https/pom.xml b/demo-https/pom.xml
new file mode 100644
index 0000000..c603394
--- /dev/null
+++ b/demo-https/pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+ demo-https
+ 0.0.1-SNAPSHOT
+ demo-https
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-https/src/main/java/com/xkcoding/https/SpringBootDemoHttpsApplication.java b/demo-https/src/main/java/com/xkcoding/https/SpringBootDemoHttpsApplication.java
new file mode 100644
index 0000000..2f7dd0a
--- /dev/null
+++ b/demo-https/src/main/java/com/xkcoding/https/SpringBootDemoHttpsApplication.java
@@ -0,0 +1,21 @@
+package com.xkcoding.https;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author Chen.Chao
+ * @date Created in 2020-01-12 10:31
+ */
+@SpringBootApplication
+public class SpringBootDemoHttpsApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoHttpsApplication.class, args);
+ }
+
+}
diff --git a/demo-https/src/main/java/com/xkcoding/https/config/HttpsConfig.java b/demo-https/src/main/java/com/xkcoding/https/config/HttpsConfig.java
new file mode 100644
index 0000000..239227a
--- /dev/null
+++ b/demo-https/src/main/java/com/xkcoding/https/config/HttpsConfig.java
@@ -0,0 +1,50 @@
+package com.xkcoding.https.config;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.connector.Connector;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ *
+ * HTTPS 配置类
+ *
+ *
+ * @author Chen.Chao
+ * @date Created in 2020-01-12 10:31
+ */
+@Configuration
+public class HttpsConfig {
+ /**
+ * 配置 http(80) -> 强制跳转到 https(443)
+ */
+ @Bean
+ public Connector connector() {
+ Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+ connector.setScheme("http");
+ connector.setPort(80);
+ connector.setSecure(false);
+ connector.setRedirectPort(443);
+ return connector;
+ }
+
+ @Bean
+ public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector) {
+ TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
+ @Override
+ protected void postProcessContext(Context context) {
+ SecurityConstraint securityConstraint = new SecurityConstraint();
+ securityConstraint.setUserConstraint("CONFIDENTIAL");
+ SecurityCollection collection = new SecurityCollection();
+ collection.addPattern("/*");
+ securityConstraint.addCollection(collection);
+ context.addConstraint(securityConstraint);
+ }
+ };
+ tomcat.addAdditionalTomcatConnectors(connector);
+ return tomcat;
+ }
+}
diff --git a/spring-boot-demo-https/src/main/resources/application.yml b/demo-https/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-https/src/main/resources/application.yml
rename to demo-https/src/main/resources/application.yml
diff --git a/spring-boot-demo-https/src/main/resources/server.keystore b/demo-https/src/main/resources/server.keystore
similarity index 100%
rename from spring-boot-demo-https/src/main/resources/server.keystore
rename to demo-https/src/main/resources/server.keystore
diff --git a/demo-https/src/main/resources/static/index.html b/demo-https/src/main/resources/static/index.html
new file mode 100644
index 0000000..933c73e
--- /dev/null
+++ b/demo-https/src/main/resources/static/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ spring boot demo https
+
+
+
+ spring boot demo https
+
+
+
+
diff --git a/spring-boot-demo-https/src/test/java/com/xkcoding/https/SpringBootDemoHttpsApplicationTests.java b/demo-https/src/test/java/com/xkcoding/https/SpringBootDemoHttpsApplicationTests.java
similarity index 100%
rename from spring-boot-demo-https/src/test/java/com/xkcoding/https/SpringBootDemoHttpsApplicationTests.java
rename to demo-https/src/test/java/com/xkcoding/https/SpringBootDemoHttpsApplicationTests.java
diff --git a/spring-boot-demo-https/ssl.png b/demo-https/ssl.png
similarity index 100%
rename from spring-boot-demo-https/ssl.png
rename to demo-https/ssl.png
diff --git a/spring-boot-demo-ldap/.gitignore b/demo-ldap/.gitignore
similarity index 100%
rename from spring-boot-demo-ldap/.gitignore
rename to demo-ldap/.gitignore
diff --git a/demo-ldap/README.md b/demo-ldap/README.md
new file mode 100644
index 0000000..916df8c
--- /dev/null
+++ b/demo-ldap/README.md
@@ -0,0 +1,393 @@
+# spring-boot-demo-ldap
+
+> 此 demo 主要演示了 Spring Boot 如何集成 `spring-boot-starter-data-ldap` 完成对 LDAP 的基本 CURD操作, 并给出以登录为实战的 API 示例
+
+## docker openldap 安装步骤
+
+> 参考: https://github.com/osixia/docker-openldap
+1. 下载镜像: `docker pull osixia/openldap:1.2.5`
+
+2. 运行容器: `docker run -p 389:389 -p 636:636 --name my-openldap --detach osixia/openldap:1.2.5`
+
+3. 添加管理员: `docker exec my-openldap ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin`
+
+4. 停止容器:`docker stop my-openldap`
+
+5. 启动容器:`docker start my-openldap`
+
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-ldap
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-ldap
+ Demo project for Spring Boot
+
+
+ spring-boot-demo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-ldap
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+ provided
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+spring:
+ ldap:
+ urls: ldap://localhost:389
+ base: dc=example,dc=org
+ username: cn=admin,dc=example,dc=org
+ password: admin
+```
+
+## Person.java
+
+> 实体类
+> @Entry 注解 映射ldap对象关系
+```java
+/**
+ * People
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 0:51
+ */
+@Data
+@Entry(
+ base = "ou=people",
+ objectClasses = {"posixAccount", "inetOrgPerson", "top"}
+)
+public class Person implements Serializable {
+
+ private static final long serialVersionUID = -7946768337975852352L;
+
+ @Id
+ private Name id;
+
+ private String uidNumber;
+
+ private String gidNumber;
+
+ /**
+ * 用户名
+ */
+ @DnAttribute(value = "uid", index = 1)
+ private String uid;
+
+ /**
+ * 姓名
+ */
+ @Attribute(name = "cn")
+ private String personName;
+
+ /**
+ * 密码
+ */
+ private String userPassword;
+
+ /**
+ * 名字
+ */
+ private String givenName;
+
+ /**
+ * 姓氏
+ */
+ @Attribute(name = "sn")
+ private String surname;
+
+ /**
+ * 邮箱
+ */
+ private String mail;
+
+ /**
+ * 职位
+ */
+ private String title;
+
+ /**
+ * 根目录
+ */
+ private String homeDirectory;
+
+ /**
+ * loginShell
+ */
+ private String loginShell;
+}
+```
+
+## PersonRepository.java
+> person 数据持久层
+```java
+/**
+ * PersonRepository
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:02
+ */
+@Repository
+public interface PersonRepository extends CrudRepository {
+
+ /**
+ * 根据用户名查找
+ *
+ * @param uid 用户名
+ * @return com.xkcoding.ldap.entity.Person
+ */
+ Person findByUid(String uid);
+}
+```
+
+## PersonService.java
+> 数据操作服务
+```java
+/**
+ * PersonService
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:05
+ */
+public interface PersonService {
+
+ /**
+ * 登录
+ *
+ * @param request {@link LoginRequest}
+ * @return {@link Result}
+ */
+ Result login(LoginRequest request);
+
+ /**
+ * 查询全部
+ *
+ * @return {@link Result}
+ */
+ Result listAllPerson();
+
+ /**
+ * 保存
+ *
+ * @param person {@link Person}
+ */
+ void save(Person person);
+
+ /**
+ * 删除
+ *
+ * @param person {@link Person}
+ */
+ void delete(Person person);
+
+}
+```
+
+## PersonServiceImpl.java
+> person数据操作服务具体逻辑实现类
+
+```java
+/**
+ * PersonServiceImpl
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:05
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class PersonServiceImpl implements PersonService {
+ private final PersonRepository personRepository;
+
+ /**
+ * 登录
+ *
+ * @param request {@link LoginRequest}
+ * @return {@link Result}
+ */
+ @Override
+ public Result login(LoginRequest request) {
+ log.info("IN LDAP auth");
+
+ Person user = personRepository.findByUid(request.getUsername());
+
+ try {
+ if (ObjectUtils.isEmpty(user)) {
+ throw new ServiceException("用户名或密码错误,请重新尝试");
+ } else {
+ user.setUserPassword(LdapUtils.asciiToString(user.getUserPassword()));
+ if (!LdapUtils.verify(user.getUserPassword(), request.getPassword())) {
+ throw new ServiceException("用户名或密码错误,请重新尝试");
+ }
+ }
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+
+ log.info("user info:{}", user);
+ return Result.success(user);
+ }
+
+ /**
+ * 查询全部
+ *
+ * @return {@link Result}
+ */
+ @Override
+ public Result listAllPerson() {
+ Iterable personList = personRepository.findAll();
+ personList.forEach(person -> person.setUserPassword(LdapUtils.asciiToString(person.getUserPassword())));
+ return Result.success(personList);
+ }
+
+ /**
+ * 保存
+ *
+ * @param person {@link Person}
+ */
+ @Override
+ public void save(Person person) {
+ Person p = personRepository.save(person);
+ log.info("用户{}保存成功", p.getUid());
+ }
+
+ /**
+ * 删除
+ *
+ * @param person {@link Person}
+ */
+ @Override
+ public void delete(Person person) {
+ personRepository.delete(person);
+ log.info("删除用户{}成功", person.getUid());
+ }
+
+}
+```
+
+## LdapDemoApplicationTests.java
+> 测试
+```java
+/**
+ * LdapDemoApplicationTest
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:06
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class LdapDemoApplicationTests {
+
+ @Resource
+ private PersonService personService;
+
+ @Test
+ public void contextLoads() {
+ }
+
+ /**
+ * 测试查询单个
+ */
+ @Test
+ public void loginTest() {
+ LoginRequest loginRequest = LoginRequest.builder().username("wangwu").password("123456").build();
+ Result login = personService.login(loginRequest);
+ System.out.println(login);
+ }
+
+ /**
+ * 测试查询列表
+ */
+ @Test
+ public void listAllPersonTest() {
+ Result result = personService.listAllPerson();
+ System.out.println(result);
+ }
+
+ /**
+ * 测试保存
+ */
+ @Test
+ public void saveTest() {
+ Person person = new Person();
+
+ person.setUid("zhaosi");
+
+ person.setSurname("赵");
+ person.setGivenName("四");
+ person.setUserPassword("123456");
+
+ // required field
+ person.setPersonName("赵四");
+ person.setUidNumber("666");
+ person.setGidNumber("666");
+ person.setHomeDirectory("/home/zhaosi");
+ person.setLoginShell("/bin/bash");
+
+ personService.save(person);
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void deleteTest() {
+ Person person = new Person();
+ person.setUid("zhaosi");
+
+ personService.delete(person);
+ }
+
+}
+```
+
+## 其余代码参见本 demo
+
+## 参考
+
+spring-data-ldap 官方文档: https://docs.spring.io/spring-data/ldap/docs/2.1.10.RELEASE/reference/html/
diff --git a/demo-ldap/pom.xml b/demo-ldap/pom.xml
new file mode 100644
index 0000000..d8134d8
--- /dev/null
+++ b/demo-ldap/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+ demo-ldap
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-ldap
+ Demo project for Spring Boot
+
+
+ spring-boot-demo
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-ldap
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+ provided
+
+
+
+
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/LdapDemoApplication.java b/demo-ldap/src/main/java/com/xkcoding/ldap/LdapDemoApplication.java
new file mode 100644
index 0000000..862f075
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/LdapDemoApplication.java
@@ -0,0 +1,19 @@
+package com.xkcoding.ldap;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * LdapDemoApplication Ldap demo 启动类
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 0:37
+ */
+@SpringBootApplication
+public class LdapDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(LdapDemoApplication.class, args);
+ }
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/api/Result.java b/demo-ldap/src/main/java/com/xkcoding/ldap/api/Result.java
new file mode 100644
index 0000000..ccf9a55
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/api/Result.java
@@ -0,0 +1,80 @@
+package com.xkcoding.ldap.api;
+
+import lombok.Data;
+import org.springframework.lang.Nullable;
+
+import java.io.Serializable;
+
+/**
+ * Result
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:44
+ */
+@Data
+public class Result implements Serializable {
+
+ private static final long serialVersionUID = 1696194043024336235L;
+
+ /**
+ * 错误码
+ */
+ private int errcode;
+
+ /**
+ * 错误信息
+ */
+ private String errmsg;
+
+ /**
+ * 响应数据
+ */
+ private T data;
+
+ public Result() {
+ }
+
+ private Result(ResultCode resultCode) {
+ this(resultCode.code, resultCode.msg);
+ }
+
+ private Result(ResultCode resultCode, T data) {
+ this(resultCode.code, resultCode.msg, data);
+ }
+
+ private Result(int errcode, String errmsg) {
+ this(errcode, errmsg, null);
+ }
+
+ private Result(int errcode, String errmsg, T data) {
+ this.errcode = errcode;
+ this.errmsg = errmsg;
+ this.data = data;
+ }
+
+
+ /**
+ * 返回成功
+ *
+ * @param 泛型标记
+ * @return 响应信息 {@code Result}
+ */
+ public static Result success() {
+ return new Result<>(ResultCode.SUCCESS);
+ }
+
+
+ /**
+ * 返回成功-携带数据
+ *
+ * @param data 响应数据
+ * @param 泛型标记
+ * @return 响应信息 {@code Result}
+ */
+ public static Result success(@Nullable T data) {
+ return new Result<>(ResultCode.SUCCESS, data);
+ }
+
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/api/ResultCode.java b/demo-ldap/src/main/java/com/xkcoding/ldap/api/ResultCode.java
new file mode 100644
index 0000000..4a40bb7
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/api/ResultCode.java
@@ -0,0 +1,31 @@
+package com.xkcoding.ldap.api;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * ResultCode
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:47
+ */
+@Getter
+@AllArgsConstructor
+public enum ResultCode {
+
+ /**
+ * 接口调用成功
+ */
+ SUCCESS(0, "Request Successful"),
+
+ /**
+ * 服务器暂不可用,建议稍候重试。建议重试次数不超过3次。
+ */
+ FAILURE(-1, "System Busy");
+
+ final int code;
+
+ final String msg;
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/entity/Person.java b/demo-ldap/src/main/java/com/xkcoding/ldap/entity/Person.java
new file mode 100644
index 0000000..ad7ebef
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/entity/Person.java
@@ -0,0 +1,92 @@
+package com.xkcoding.ldap.entity;
+
+import lombok.Data;
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.DnAttribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+
+import javax.naming.Name;
+import java.io.Serializable;
+
+/**
+ * People
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 0:51
+ */
+@Data
+@Entry(base = "ou=people", objectClasses = {"posixAccount", "inetOrgPerson", "top"})
+public class Person implements Serializable {
+
+ private static final long serialVersionUID = -7946768337975852352L;
+
+ @Id
+ private Name id;
+
+ /**
+ * 用户id
+ */
+ private String uidNumber;
+
+ /**
+ * 用户名
+ */
+ @DnAttribute(value = "uid", index = 1)
+ private String uid;
+
+ /**
+ * 姓名
+ */
+ @Attribute(name = "cn")
+ private String personName;
+
+ /**
+ * 密码
+ */
+ private String userPassword;
+
+ /**
+ * 名字
+ */
+ private String givenName;
+
+ /**
+ * 姓氏
+ */
+ @Attribute(name = "sn")
+ private String surname;
+
+ /**
+ * 邮箱
+ */
+ private String mail;
+
+ /**
+ * 职位
+ */
+ private String title;
+
+ /**
+ * 部门
+ */
+ private String departmentNumber;
+
+ /**
+ * 部门id
+ */
+ private String gidNumber;
+
+ /**
+ * 根目录
+ */
+ private String homeDirectory;
+
+ /**
+ * loginShell
+ */
+ private String loginShell;
+
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/exception/ServiceException.java b/demo-ldap/src/main/java/com/xkcoding/ldap/exception/ServiceException.java
new file mode 100644
index 0000000..e84471a
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/exception/ServiceException.java
@@ -0,0 +1,47 @@
+package com.xkcoding.ldap.exception;
+
+import com.xkcoding.ldap.api.ResultCode;
+import lombok.Getter;
+
+/**
+ * ServiceException
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:53
+ */
+public class ServiceException extends RuntimeException {
+
+ @Getter
+ private int errcode;
+
+ @Getter
+ private String errmsg;
+
+ public ServiceException(ResultCode resultCode) {
+ this(resultCode.getCode(), resultCode.getMsg());
+ }
+
+ public ServiceException(String message) {
+ super(message);
+ }
+
+ public ServiceException(Integer errcode, String errmsg) {
+ super(errmsg);
+ this.errcode = errcode;
+ this.errmsg = errmsg;
+ }
+
+ public ServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ServiceException(Throwable cause) {
+ super(cause);
+ }
+
+ public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/repository/PersonRepository.java b/demo-ldap/src/main/java/com/xkcoding/ldap/repository/PersonRepository.java
new file mode 100644
index 0000000..5939e2d
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/repository/PersonRepository.java
@@ -0,0 +1,26 @@
+package com.xkcoding.ldap.repository;
+
+import com.xkcoding.ldap.entity.Person;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import javax.naming.Name;
+
+/**
+ * PersonRepository
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:02
+ */
+@Repository
+public interface PersonRepository extends CrudRepository {
+
+ /**
+ * 根据用户名查找
+ *
+ * @param uid 用户名
+ * @return com.xkcoding.ldap.entity.Person
+ */
+ Person findByUid(String uid);
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/request/LoginRequest.java b/demo-ldap/src/main/java/com/xkcoding/ldap/request/LoginRequest.java
new file mode 100644
index 0000000..34bcafd
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/request/LoginRequest.java
@@ -0,0 +1,21 @@
+package com.xkcoding.ldap.request;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * LoginRequest
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:50
+ */
+@Data
+@Builder
+public class LoginRequest {
+
+ private String username;
+
+ private String password;
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/service/PersonService.java b/demo-ldap/src/main/java/com/xkcoding/ldap/service/PersonService.java
new file mode 100644
index 0000000..c5a07be
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/service/PersonService.java
@@ -0,0 +1,45 @@
+package com.xkcoding.ldap.service;
+
+import com.xkcoding.ldap.api.Result;
+import com.xkcoding.ldap.entity.Person;
+import com.xkcoding.ldap.request.LoginRequest;
+
+/**
+ * PersonService
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:05
+ */
+public interface PersonService {
+
+ /**
+ * 登录
+ *
+ * @param request {@link LoginRequest}
+ * @return {@link Result}
+ */
+ Result login(LoginRequest request);
+
+ /**
+ * 查询全部
+ *
+ * @return {@link Result}
+ */
+ Result listAllPerson();
+
+ /**
+ * 保存
+ *
+ * @param person {@link Person}
+ */
+ void save(Person person);
+
+ /**
+ * 删除
+ *
+ * @param person {@link Person}
+ */
+ void delete(Person person);
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/service/impl/PersonServiceImpl.java b/demo-ldap/src/main/java/com/xkcoding/ldap/service/impl/PersonServiceImpl.java
new file mode 100644
index 0000000..05ee7aa
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/service/impl/PersonServiceImpl.java
@@ -0,0 +1,94 @@
+package com.xkcoding.ldap.service.impl;
+
+import com.xkcoding.ldap.api.Result;
+import com.xkcoding.ldap.entity.Person;
+import com.xkcoding.ldap.exception.ServiceException;
+import com.xkcoding.ldap.repository.PersonRepository;
+import com.xkcoding.ldap.request.LoginRequest;
+import com.xkcoding.ldap.service.PersonService;
+import com.xkcoding.ldap.util.LdapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * PersonServiceImpl
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:05
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class PersonServiceImpl implements PersonService {
+ private final PersonRepository personRepository;
+
+ /**
+ * 登录
+ *
+ * @param request {@link LoginRequest}
+ * @return {@link Result}
+ */
+ @Override
+ public Result login(LoginRequest request) {
+ log.info("IN LDAP auth");
+
+ Person user = personRepository.findByUid(request.getUsername());
+
+ try {
+ if (ObjectUtils.isEmpty(user)) {
+ throw new ServiceException("用户名或密码错误,请重新尝试");
+ } else {
+ user.setUserPassword(LdapUtils.asciiToString(user.getUserPassword()));
+ if (!LdapUtils.verify(user.getUserPassword(), request.getPassword())) {
+ throw new ServiceException("用户名或密码错误,请重新尝试");
+ }
+ }
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+
+ log.info("user info:{}", user);
+ return Result.success(user);
+ }
+
+ /**
+ * 查询全部
+ *
+ * @return {@link Result}
+ */
+ @Override
+ public Result listAllPerson() {
+ Iterable personList = personRepository.findAll();
+ personList.forEach(person -> person.setUserPassword(LdapUtils.asciiToString(person.getUserPassword())));
+ return Result.success(personList);
+ }
+
+ /**
+ * 保存
+ *
+ * @param person {@link Person}
+ */
+ @Override
+ public void save(Person person) {
+ Person p = personRepository.save(person);
+ log.info("用户{}保存成功", p.getUid());
+ }
+
+ /**
+ * 删除
+ *
+ * @param person {@link Person}
+ */
+ @Override
+ public void delete(Person person) {
+ personRepository.delete(person);
+ log.info("删除用户{}成功", person.getUid());
+ }
+
+}
diff --git a/demo-ldap/src/main/java/com/xkcoding/ldap/util/LdapUtils.java b/demo-ldap/src/main/java/com/xkcoding/ldap/util/LdapUtils.java
new file mode 100644
index 0000000..5b9ede3
--- /dev/null
+++ b/demo-ldap/src/main/java/com/xkcoding/ldap/util/LdapUtils.java
@@ -0,0 +1,78 @@
+package com.xkcoding.ldap.util;
+
+import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * LdapUtils
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:03
+ */
+public class LdapUtils {
+
+ /**
+ * 校验密码
+ *
+ * @param ldapPassword ldap 加密密码
+ * @param inputPassword 用户输入
+ * @return boolean
+ * @throws NoSuchAlgorithmException 加解密异常
+ */
+ public static boolean verify(String ldapPassword, String inputPassword) throws NoSuchAlgorithmException {
+
+ // MessageDigest 提供了消息摘要算法,如 MD5 或 SHA,的功能,这里LDAP使用的是SHA-1
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+
+ // 取出加密字符
+ if (ldapPassword.startsWith("{SSHA}")) {
+ ldapPassword = ldapPassword.substring(6);
+ } else if (ldapPassword.startsWith("{SHA}")) {
+ ldapPassword = ldapPassword.substring(5);
+ }
+ // 解码BASE64
+ byte[] ldapPasswordByte = Base64.decode(ldapPassword);
+ byte[] shaCode;
+ byte[] salt;
+
+ // 前20位是SHA-1加密段,20位后是最初加密时的随机明文
+ if (ldapPasswordByte.length <= 20) {
+ shaCode = ldapPasswordByte;
+ salt = new byte[0];
+ } else {
+ shaCode = new byte[20];
+ salt = new byte[ldapPasswordByte.length - 20];
+ System.arraycopy(ldapPasswordByte, 0, shaCode, 0, 20);
+ System.arraycopy(ldapPasswordByte, 20, salt, 0, salt.length);
+ }
+ // 把用户输入的密码添加到摘要计算信息
+ md.update(inputPassword.getBytes());
+ // 把随机明文添加到摘要计算信息
+ md.update(salt);
+
+ // 按SSHA把当前用户密码进行计算
+ byte[] inputPasswordByte = md.digest();
+
+ // 返回校验结果
+ return MessageDigest.isEqual(shaCode, inputPasswordByte);
+ }
+
+ /**
+ * Ascii转换为字符串
+ *
+ * @param value Ascii串
+ * @return 字符串
+ */
+ public static String asciiToString(String value) {
+ StringBuilder sbu = new StringBuilder();
+ String[] chars = value.split(",");
+ for (String aChar : chars) {
+ sbu.append((char) Integer.parseInt(aChar));
+ }
+ return sbu.toString();
+ }
+
+}
diff --git a/spring-boot-demo-ldap/src/main/resources/application.yml b/demo-ldap/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-ldap/src/main/resources/application.yml
rename to demo-ldap/src/main/resources/application.yml
diff --git a/demo-ldap/src/test/java/com/xkcoding/ldap/LdapDemoApplicationTests.java b/demo-ldap/src/test/java/com/xkcoding/ldap/LdapDemoApplicationTests.java
new file mode 100644
index 0000000..377a097
--- /dev/null
+++ b/demo-ldap/src/test/java/com/xkcoding/ldap/LdapDemoApplicationTests.java
@@ -0,0 +1,85 @@
+package com.xkcoding.ldap;
+
+import com.xkcoding.ldap.api.Result;
+import com.xkcoding.ldap.entity.Person;
+import com.xkcoding.ldap.request.LoginRequest;
+import com.xkcoding.ldap.service.PersonService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.annotation.Resource;
+
+/**
+ * LdapDemoApplicationTest
+ *
+ * @author fxbin
+ * @version v1.0
+ * @since 2019-08-26 1:06
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class LdapDemoApplicationTests {
+
+ @Resource
+ private PersonService personService;
+
+ @Test
+ public void contextLoads() {
+ }
+
+ /**
+ * 测试查询单个
+ */
+ @Test
+ public void loginTest() {
+ LoginRequest loginRequest = LoginRequest.builder().username("wangwu").password("123456").build();
+ Result login = personService.login(loginRequest);
+ System.out.println(login);
+ }
+
+ /**
+ * 测试查询列表
+ */
+ @Test
+ public void listAllPersonTest() {
+ Result result = personService.listAllPerson();
+ System.out.println(result);
+ }
+
+ /**
+ * 测试保存
+ */
+ @Test
+ public void saveTest() {
+ Person person = new Person();
+
+ person.setUid("zhaosi");
+
+ person.setSurname("赵");
+ person.setGivenName("四");
+ person.setUserPassword("123456");
+
+ // required field
+ person.setPersonName("赵四");
+ person.setUidNumber("666");
+ person.setGidNumber("666");
+ person.setHomeDirectory("/home/zhaosi");
+ person.setLoginShell("/bin/bash");
+
+ personService.save(person);
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void deleteTest() {
+ Person person = new Person();
+ person.setUid("zhaosi");
+
+ personService.delete(person);
+ }
+
+}
diff --git a/spring-boot-demo-log-aop/.gitignore b/demo-log-aop/.gitignore
similarity index 100%
rename from spring-boot-demo-log-aop/.gitignore
rename to demo-log-aop/.gitignore
diff --git a/demo-log-aop/README.md b/demo-log-aop/README.md
new file mode 100644
index 0000000..14fea80
--- /dev/null
+++ b/demo-log-aop/README.md
@@ -0,0 +1,281 @@
+# spring-boot-demo-log-aop
+
+> 此 demo 主要是演示如何使用 aop 切面对请求进行日志记录,并且记录 UserAgent 信息。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-log-aop
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-log-aop
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ eu.bitwalker
+ UserAgentUtils
+
+
+
+
+ spring-boot-demo-log-aop
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## AopLog.java
+
+```java
+/**
+ *
+ * 使用 aop 切面记录请求日志信息
+ *
+ *
+ * @author yangkai.shen
+ * @author chen qi
+ * @date Created in 2018-10-01 22:05
+ */
+@Aspect
+@Component
+@Slf4j
+public class AopLog {
+ /**
+ * 切入点
+ */
+ @Pointcut("execution(public * com.xkcoding.log.aop.controller.*Controller.*(..))")
+ public void log() {
+
+ }
+
+ /**
+ * 环绕操作
+ *
+ * @param point 切入点
+ * @return 原方法返回值
+ * @throws Throwable 异常信息
+ */
+ @Around("log()")
+ public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
+
+ // 开始打印请求日志
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
+
+ // 打印请求相关参数
+ long startTime = System.currentTimeMillis();
+ Object result = point.proceed();
+ String header = request.getHeader("User-Agent");
+ UserAgent userAgent = UserAgent.parseUserAgentString(header);
+
+ final Log l = Log.builder()
+ .threadId(Long.toString(Thread.currentThread().getId()))
+ .threadName(Thread.currentThread().getName())
+ .ip(getIp(request))
+ .url(request.getRequestURL().toString())
+ .classMethod(String.format("%s.%s", point.getSignature().getDeclaringTypeName(),
+ point.getSignature().getName()))
+ .httpMethod(request.getMethod())
+ .requestParams(getNameAndValue(point))
+ .result(result)
+ .timeCost(System.currentTimeMillis() - startTime)
+ .userAgent(header)
+ .browser(userAgent.getBrowser().toString())
+ .os(userAgent.getOperatingSystem().toString()).build();
+
+ log.info("Request Log Info : {}", JSONUtil.toJsonStr(l));
+
+ return result;
+ }
+
+ /**
+ * 获取方法参数名和参数值
+ * @param joinPoint
+ * @return
+ */
+ private Map getNameAndValue(ProceedingJoinPoint joinPoint) {
+
+ final Signature signature = joinPoint.getSignature();
+ MethodSignature methodSignature = (MethodSignature) signature;
+ final String[] names = methodSignature.getParameterNames();
+ final Object[] args = joinPoint.getArgs();
+
+ if (ArrayUtil.isEmpty(names) || ArrayUtil.isEmpty(args)) {
+ return Collections.emptyMap();
+ }
+ if (names.length != args.length) {
+ log.warn("{}方法参数名和参数值数量不一致", methodSignature.getName());
+ return Collections.emptyMap();
+ }
+ Map map = Maps.newHashMap();
+ for (int i = 0; i < names.length; i++) {
+ map.put(names[i], args[i]);
+ }
+ return map;
+ }
+
+ private static final String UNKNOWN = "unknown";
+
+ /**
+ * 获取ip地址
+ */
+ public static String getIp(HttpServletRequest request) {
+ String ip = request.getHeader("x-forwarded-for");
+ if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+ String comma = ",";
+ String localhost = "127.0.0.1";
+ if (ip.contains(comma)) {
+ ip = ip.split(",")[0];
+ }
+ if (localhost.equals(ip)) {
+ // 获取本机真正的ip地址
+ try {
+ ip = InetAddress.getLocalHost().getHostAddress();
+ } catch (UnknownHostException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return ip;
+ }
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ static class Log {
+ // 线程id
+ private String threadId;
+ // 线程名称
+ private String threadName;
+ // ip
+ private String ip;
+ // url
+ private String url;
+ // http方法 GET POST PUT DELETE PATCH
+ private String httpMethod;
+ // 类方法
+ private String classMethod;
+ // 请求参数
+ private Object requestParams;
+ // 返回参数
+ private Object result;
+ // 接口耗时
+ private Long timeCost;
+ // 操作系统
+ private String os;
+ // 浏览器
+ private String browser;
+ // user-agent
+ private String userAgent;
+ }
+}
+```
+
+## TestController.java
+
+```java
+/**
+ *
+ * 测试 Controller
+ *
+ *
+ * @author yangkai.shen
+ * @author chen qi
+ * @date Created in 2018-10-01 22:10
+ */
+@Slf4j
+@RestController
+public class TestController {
+
+ /**
+ * 测试方法
+ *
+ * @param who 测试参数
+ * @return {@link Dict}
+ */
+ @GetMapping("/test")
+ public Dict test(String who) {
+ return Dict.create().set("who", StrUtil.isBlank(who) ? "me" : who);
+ }
+
+ /**
+ * 测试post json方法
+ * @param map 请求的json参数
+ * @return {@link Dict}
+ */
+ @PostMapping("/testJson")
+ public Dict testJson(@RequestBody Map map) {
+
+ final String jsonStr = JSONUtil.toJsonStr(map);
+ log.info(jsonStr);
+ return Dict.create().set("json", map);
+ }
+}
+```
+
diff --git a/demo-log-aop/pom.xml b/demo-log-aop/pom.xml
new file mode 100644
index 0000000..a01664f
--- /dev/null
+++ b/demo-log-aop/pom.xml
@@ -0,0 +1,76 @@
+
+
+ 4.0.0
+
+ demo-log-aop
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-log-aop
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ eu.bitwalker
+ UserAgentUtils
+
+
+
+
+ demo-log-aop
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-log-aop/src/main/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplication.java b/demo-log-aop/src/main/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplication.java
new file mode 100644
index 0000000..32d225c
--- /dev/null
+++ b/demo-log-aop/src/main/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.log.aop;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-10-01 22:05
+ */
+@SpringBootApplication
+public class SpringBootDemoLogAopApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoLogAopApplication.class, args);
+ }
+}
diff --git a/demo-log-aop/src/main/java/com/xkcoding/log/aop/aspectj/AopLog.java b/demo-log-aop/src/main/java/com/xkcoding/log/aop/aspectj/AopLog.java
new file mode 100644
index 0000000..d39c093
--- /dev/null
+++ b/demo-log-aop/src/main/java/com/xkcoding/log/aop/aspectj/AopLog.java
@@ -0,0 +1,178 @@
+package com.xkcoding.log.aop.aspectj;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.collect.Maps;
+import eu.bitwalker.useragentutils.UserAgent;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ *
+ * 使用 aop 切面记录请求日志信息
+ *
+ *
+ * @author yangkai.shen
+ * @author chen qi
+ * @date Created in 2018-10-01 22:05
+ */
+@Aspect
+@Component
+@Slf4j
+public class AopLog {
+ /**
+ * 切入点
+ */
+ @Pointcut("execution(public * com.xkcoding.log.aop.controller.*Controller.*(..))")
+ public void log() {
+
+ }
+
+ /**
+ * 环绕操作
+ *
+ * @param point 切入点
+ * @return 原方法返回值
+ * @throws Throwable 异常信息
+ */
+ @Around("log()")
+ public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
+
+ // 开始打印请求日志
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
+
+ // 打印请求相关参数
+ long startTime = System.currentTimeMillis();
+ Object result = point.proceed();
+ String header = request.getHeader("User-Agent");
+ UserAgent userAgent = UserAgent.parseUserAgentString(header);
+
+ final Log l = Log.builder()
+ .threadId(Long.toString(Thread.currentThread().getId()))
+ .threadName(Thread.currentThread().getName())
+ .ip(getIp(request))
+ .url(request.getRequestURL().toString())
+ .classMethod(String.format("%s.%s", point.getSignature().getDeclaringTypeName(),
+ point.getSignature().getName()))
+ .httpMethod(request.getMethod())
+ .requestParams(getNameAndValue(point))
+ .result(result)
+ .timeCost(System.currentTimeMillis() - startTime)
+ .userAgent(header)
+ .browser(userAgent.getBrowser().toString())
+ .os(userAgent.getOperatingSystem().toString()).build();
+
+ log.info("Request Log Info : {}", JSONUtil.toJsonStr(l));
+
+ return result;
+ }
+
+ /**
+ * 获取方法参数名和参数值
+ * @param joinPoint
+ * @return
+ */
+ private Map getNameAndValue(ProceedingJoinPoint joinPoint) {
+
+ final Signature signature = joinPoint.getSignature();
+ MethodSignature methodSignature = (MethodSignature) signature;
+ final String[] names = methodSignature.getParameterNames();
+ final Object[] args = joinPoint.getArgs();
+
+ if (ArrayUtil.isEmpty(names) || ArrayUtil.isEmpty(args)) {
+ return Collections.emptyMap();
+ }
+ if (names.length != args.length) {
+ log.warn("{}方法参数名和参数值数量不一致", methodSignature.getName());
+ return Collections.emptyMap();
+ }
+ Map map = Maps.newHashMap();
+ for (int i = 0; i < names.length; i++) {
+ map.put(names[i], args[i]);
+ }
+ return map;
+ }
+
+ private static final String UNKNOWN = "unknown";
+
+ /**
+ * 获取ip地址
+ */
+ public static String getIp(HttpServletRequest request) {
+ String ip = request.getHeader("x-forwarded-for");
+ if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+ String comma = ",";
+ String localhost = "127.0.0.1";
+ if (ip.contains(comma)) {
+ ip = ip.split(",")[0];
+ }
+ if (localhost.equals(ip)) {
+ // 获取本机真正的ip地址
+ try {
+ ip = InetAddress.getLocalHost().getHostAddress();
+ } catch (UnknownHostException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return ip;
+ }
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ static class Log {
+ // 线程id
+ private String threadId;
+ // 线程名称
+ private String threadName;
+ // ip
+ private String ip;
+ // url
+ private String url;
+ // http方法 GET POST PUT DELETE PATCH
+ private String httpMethod;
+ // 类方法
+ private String classMethod;
+ // 请求参数
+ private Object requestParams;
+ // 返回参数
+ private Object result;
+ // 接口耗时
+ private Long timeCost;
+ // 操作系统
+ private String os;
+ // 浏览器
+ private String browser;
+ // user-agent
+ private String userAgent;
+ }
+}
diff --git a/demo-log-aop/src/main/java/com/xkcoding/log/aop/controller/TestController.java b/demo-log-aop/src/main/java/com/xkcoding/log/aop/controller/TestController.java
new file mode 100644
index 0000000..c261d79
--- /dev/null
+++ b/demo-log-aop/src/main/java/com/xkcoding/log/aop/controller/TestController.java
@@ -0,0 +1,50 @@
+package com.xkcoding.log.aop.controller;
+
+import cn.hutool.core.lang.Dict;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ *
+ * 测试 Controller
+ *
+ *
+ * @author yangkai.shen
+ * @author chen qi
+ * @date Created in 2018-10-01 22:10
+ */
+@Slf4j
+@RestController
+public class TestController {
+
+ /**
+ * 测试方法
+ *
+ * @param who 测试参数
+ * @return {@link Dict}
+ */
+ @GetMapping("/test")
+ public Dict test(String who) {
+ return Dict.create().set("who", StrUtil.isBlank(who) ? "me" : who);
+ }
+
+ /**
+ * 测试post json方法
+ * @param map 请求的json参数
+ * @return {@link Dict}
+ */
+ @PostMapping("/testJson")
+ public Dict testJson(@RequestBody Map map) {
+
+ final String jsonStr = JSONUtil.toJsonStr(map);
+ log.info(jsonStr);
+ return Dict.create().set("json", map);
+ }
+}
diff --git a/spring-boot-demo-log-aop/src/main/resources/application.yml b/demo-log-aop/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-log-aop/src/main/resources/application.yml
rename to demo-log-aop/src/main/resources/application.yml
diff --git a/demo-log-aop/src/main/resources/logback-spring.xml b/demo-log-aop/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..86bf301
--- /dev/null
+++ b/demo-log-aop/src/main/resources/logback-spring.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+ INFO
+
+
+ %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n
+ UTF-8
+
+
+
+
+
+
+
+ ERROR
+
+ DENY
+
+ ACCEPT
+
+
+
+
+
+
+ logs/demo-log-aop/info.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+
+
+ 2MB
+
+
+
+
+
+
+ %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n
+ UTF-8
+
+
+
+
+
+
+ Error
+
+
+
+
+
+
+ logs/demo-log-aop/error.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+ 2MB
+
+
+
+ %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n
+ UTF-8
+
+
+
+
+
+
+
+
+
diff --git a/demo-log-aop/src/test/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplicationTests.java b/demo-log-aop/src/test/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplicationTests.java
new file mode 100644
index 0000000..af44ad9
--- /dev/null
+++ b/demo-log-aop/src/test/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.log.aop;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoLogAopApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/spring-boot-demo-logback/.gitignore b/demo-logback/.gitignore
similarity index 100%
rename from spring-boot-demo-logback/.gitignore
rename to demo-logback/.gitignore
diff --git a/demo-logback/README.md b/demo-logback/README.md
new file mode 100644
index 0000000..d7d6988
--- /dev/null
+++ b/demo-logback/README.md
@@ -0,0 +1,178 @@
+# spring-boot-demo-logback
+
+> 此 demo 主要演示了如何使用 logback 记录程序运行过程中的日志,以及如何配置 logback,可以同时生成控制台日志和文件日志记录,文件日志以日期和大小进行拆分生成。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-logback
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-logback
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-logback
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## SpringBootDemoLogbackApplication.java
+
+```java
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-09-30 23:16
+ */
+@SpringBootApplication
+@Slf4j
+public class SpringBootDemoLogbackApplication {
+
+ public static void main(String[] args) {
+ ConfigurableApplicationContext context = SpringApplication.run(SpringBootDemoLogbackApplication.class, args);
+ int length = context.getBeanDefinitionNames().length;
+ log.trace("Spring boot启动初始化了 {} 个 Bean", length);
+ log.debug("Spring boot启动初始化了 {} 个 Bean", length);
+ log.info("Spring boot启动初始化了 {} 个 Bean", length);
+ log.warn("Spring boot启动初始化了 {} 个 Bean", length);
+ log.error("Spring boot启动初始化了 {} 个 Bean", length);
+ try {
+ int i = 0;
+ int j = 1 / i;
+ } catch (Exception e) {
+ log.error("【SpringBootDemoLogbackApplication】启动异常:", e);
+ }
+ }
+}
+```
+
+## logback-spring.xml
+
+```xml
+
+
+
+
+
+ INFO
+
+
+ %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n
+ UTF-8
+
+
+
+
+
+
+
+ ERROR
+
+ DENY
+
+ ACCEPT
+
+
+
+
+
+
+ logs/spring-boot-demo-logback/info.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+
+
+ 2MB
+
+
+
+
+
+
+ %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n
+ UTF-8
+
+
+
+
+
+
+ Error
+
+
+
+
+
+
+ logs/spring-boot-demo-logback/error.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+ 2MB
+
+
+
+ %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n
+ UTF-8
+
+
+
+
+
+
+
+
+
+```
+
diff --git a/demo-logback/pom.xml b/demo-logback/pom.xml
new file mode 100644
index 0000000..174a1cd
--- /dev/null
+++ b/demo-logback/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ demo-logback
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-logback
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-logback
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-logback/src/main/java/com/xkcoding/logback/SpringBootDemoLogbackApplication.java b/demo-logback/src/main/java/com/xkcoding/logback/SpringBootDemoLogbackApplication.java
new file mode 100644
index 0000000..217ee02
--- /dev/null
+++ b/demo-logback/src/main/java/com/xkcoding/logback/SpringBootDemoLogbackApplication.java
@@ -0,0 +1,35 @@
+package com.xkcoding.logback;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+
+/**
+ *
+ * 启动类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-09-30 23:16
+ */
+@SpringBootApplication
+@Slf4j
+public class SpringBootDemoLogbackApplication {
+
+ public static void main(String[] args) {
+ ConfigurableApplicationContext context = SpringApplication.run(SpringBootDemoLogbackApplication.class, args);
+ int length = context.getBeanDefinitionNames().length;
+ log.trace("Spring boot启动初始化了 {} 个 Bean", length);
+ log.debug("Spring boot启动初始化了 {} 个 Bean", length);
+ log.info("Spring boot启动初始化了 {} 个 Bean", length);
+ log.warn("Spring boot启动初始化了 {} 个 Bean", length);
+ log.error("Spring boot启动初始化了 {} 个 Bean", length);
+ try {
+ int i = 0;
+ int j = 1 / i;
+ } catch (Exception e) {
+ log.error("【SpringBootDemoLogbackApplication】启动异常:", e);
+ }
+ }
+}
diff --git a/spring-boot-demo-logback/src/main/resources/application.yml b/demo-logback/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-logback/src/main/resources/application.yml
rename to demo-logback/src/main/resources/application.yml
diff --git a/demo-logback/src/main/resources/logback-spring.xml b/demo-logback/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..dcd48fe
--- /dev/null
+++ b/demo-logback/src/main/resources/logback-spring.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+ INFO
+
+
+ ${CONSOLE_LOG_PATTERN}
+ UTF-8
+
+
+
+
+
+
+
+ ERROR
+
+ DENY
+
+ ACCEPT
+
+
+
+
+
+
+ logs/demo-logback/info.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+
+
+ 2MB
+
+
+
+
+
+
+ ${FILE_LOG_PATTERN}
+ UTF-8
+
+
+
+
+
+
+ Error
+
+
+
+
+
+
+ logs/demo-logback/error.created_on_%d{yyyy-MM-dd}.part_%i.log
+
+ 90
+
+
+ 2MB
+
+
+
+ ${FILE_ERROR_PATTERN}
+ UTF-8
+
+
+
+
+
+
+
+
+
diff --git a/demo-logback/src/test/java/com/xkcoding/logback/SpringBootDemoLogbackApplicationTests.java b/demo-logback/src/test/java/com/xkcoding/logback/SpringBootDemoLogbackApplicationTests.java
new file mode 100644
index 0000000..53bbb7f
--- /dev/null
+++ b/demo-logback/src/test/java/com/xkcoding/logback/SpringBootDemoLogbackApplicationTests.java
@@ -0,0 +1,16 @@
+package com.xkcoding.logback;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoLogbackApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/spring-boot-demo-mongodb/.gitignore b/demo-mongodb/.gitignore
similarity index 100%
rename from spring-boot-demo-mongodb/.gitignore
rename to demo-mongodb/.gitignore
diff --git a/demo-mongodb/README.md b/demo-mongodb/README.md
new file mode 100644
index 0000000..2c6b85d
--- /dev/null
+++ b/demo-mongodb/README.md
@@ -0,0 +1,317 @@
+# spring-boot-demo-mongodb
+
+> 此 demo 主要演示了 Spring Boot 如何集成 MongoDB,使用官方的 starter 实现增删改查。
+
+## 注意
+
+作者编写本demo时,MongoDB 最新版本为 `4.1`,使用 docker 运行,下面是所有步骤:
+
+1. 下载镜像:`docker pull mongo:4.1`
+2. 运行容器:`docker run -d -p 27017:27017 -v /Users/yangkai.shen/docker/mongo/data:/data/db --name mongo-4.1 mongo:4.1`
+3. 停止容器:`docker stop mongo-4.1`
+4. 启动容器:`docker start mongo-4.1`
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-mongodb
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-mongodb
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-mongodb
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+spring:
+ data:
+ mongodb:
+ host: localhost
+ port: 27017
+ database: article_db
+logging:
+ level:
+ org.springframework.data.mongodb.core: debug
+```
+
+## Article.java
+
+```java
+/**
+ *
+ * 文章实体类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:21
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Article {
+ /**
+ * 文章id
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 文章标题
+ */
+ private String title;
+
+ /**
+ * 文章内容
+ */
+ private String content;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+ /**
+ * 更新时间
+ */
+ private Date updateTime;
+
+ /**
+ * 点赞数量
+ */
+ private Long thumbUp;
+
+ /**
+ * 访客数量
+ */
+ private Long visits;
+
+}
+```
+
+## ArticleRepository.java
+
+```java
+/**
+ *
+ * 文章 Dao
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:30
+ */
+public interface ArticleRepository extends MongoRepository {
+ /**
+ * 根据标题模糊查询
+ *
+ * @param title 标题
+ * @return 满足条件的文章列表
+ */
+ List findByTitleLike(String title);
+}
+```
+
+## ArticleRepositoryTest.java
+
+```java
+/**
+ *
+ * 测试操作 MongoDb
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:35
+ */
+@Slf4j
+public class ArticleRepositoryTest extends SpringBootDemoMongodbApplicationTests {
+ @Autowired
+ private ArticleRepository articleRepo;
+
+ @Autowired
+ private MongoTemplate mongoTemplate;
+
+ @Autowired
+ private Snowflake snowflake;
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void testSave() {
+ Article article = new Article(1L, RandomUtil.randomString(20), RandomUtil.randomString(150), DateUtil.date(), DateUtil
+ .date(), 0L, 0L);
+ articleRepo.save(article);
+ log.info("【article】= {}", JSONUtil.toJsonStr(article));
+ }
+
+ /**
+ * 测试新增列表
+ */
+ @Test
+ public void testSaveList() {
+ List articles = Lists.newArrayList();
+ for (int i = 0; i < 10; i++) {
+ articles.add(new Article(snowflake.nextId(), RandomUtil.randomString(20), RandomUtil.randomString(150), DateUtil
+ .date(), DateUtil.date(), 0L, 0L));
+ }
+ articleRepo.saveAll(articles);
+
+ log.info("【articles】= {}", JSONUtil.toJsonStr(articles.stream()
+ .map(Article::getId)
+ .collect(Collectors.toList())));
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void testUpdate() {
+ articleRepo.findById(1L).ifPresent(article -> {
+ article.setTitle(article.getTitle() + "更新之后的标题");
+ article.setUpdateTime(DateUtil.date());
+ articleRepo.save(article);
+ log.info("【article】= {}", JSONUtil.toJsonStr(article));
+ });
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void testDelete() {
+ // 根据主键删除
+ articleRepo.deleteById(1L);
+
+ // 全部删除
+ articleRepo.deleteAll();
+ }
+
+ /**
+ * 测试点赞数、访客数,使用save方式更新点赞、访客
+ */
+ @Test
+ public void testThumbUp() {
+ articleRepo.findById(1L).ifPresent(article -> {
+ article.setThumbUp(article.getThumbUp() + 1);
+ article.setVisits(article.getVisits() + 1);
+ articleRepo.save(article);
+ log.info("【标题】= {}【点赞数】= {}【访客数】= {}", article.getTitle(), article.getThumbUp(), article.getVisits());
+ });
+ }
+
+ /**
+ * 测试点赞数、访客数,使用更优雅/高效的方式更新点赞、访客
+ */
+ @Test
+ public void testThumbUp2() {
+ Query query = new Query();
+ query.addCriteria(Criteria.where("_id").is(1L));
+ Update update = new Update();
+ update.inc("thumbUp", 1L);
+ update.inc("visits", 1L);
+ mongoTemplate.updateFirst(query, update, "article");
+
+ articleRepo.findById(1L)
+ .ifPresent(article -> log.info("【标题】= {}【点赞数】= {}【访客数】= {}", article.getTitle(), article.getThumbUp(), article
+ .getVisits()));
+ }
+
+ /**
+ * 测试分页排序查询
+ */
+ @Test
+ public void testQuery() {
+ Sort sort = Sort.by("thumbUp", "updateTime").descending();
+ PageRequest pageRequest = PageRequest.of(0, 5, sort);
+ Page all = articleRepo.findAll(pageRequest);
+ log.info("【总页数】= {}", all.getTotalPages());
+ log.info("【总条数】= {}", all.getTotalElements());
+ log.info("【当前页数据】= {}", JSONUtil.toJsonStr(all.getContent()
+ .stream()
+ .map(article -> "文章标题:" + article.getTitle() + "点赞数:" + article.getThumbUp() + "更新时间:" + article.getUpdateTime())
+ .collect(Collectors.toList())));
+ }
+
+ /**
+ * 测试根据标题模糊查询
+ */
+ @Test
+ public void testFindByTitleLike() {
+ List articles = articleRepo.findByTitleLike("更新");
+ log.info("【articles】= {}", JSONUtil.toJsonStr(articles));
+ }
+
+}
+```
+
+## 参考
+
+1. Spring Data MongoDB 官方文档:https://docs.spring.io/spring-data/mongodb/docs/2.1.2.RELEASE/reference/html/
+2. MongoDB 官方镜像地址:https://hub.docker.com/_/mongo
+3. MongoDB 官方快速入门:https://docs.mongodb.com/manual/tutorial/getting-started/
+4. MongoDB 官方文档:https://docs.mongodb.com/manual/
diff --git a/demo-mongodb/pom.xml b/demo-mongodb/pom.xml
new file mode 100644
index 0000000..b63c3d4
--- /dev/null
+++ b/demo-mongodb/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ demo-mongodb
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-mongodb
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-mongodb
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-mongodb/src/main/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplication.java b/demo-mongodb/src/main/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplication.java
new file mode 100644
index 0000000..8c6fd8b
--- /dev/null
+++ b/demo-mongodb/src/main/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplication.java
@@ -0,0 +1,30 @@
+package com.xkcoding.mongodb;
+
+import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.util.IdUtil;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:14
+ */
+@SpringBootApplication
+public class SpringBootDemoMongodbApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoMongodbApplication.class, args);
+ }
+
+ @Bean
+ public Snowflake snowflake() {
+ return IdUtil.createSnowflake(1, 1);
+ }
+
+}
+
diff --git a/demo-mongodb/src/main/java/com/xkcoding/mongodb/model/Article.java b/demo-mongodb/src/main/java/com/xkcoding/mongodb/model/Article.java
new file mode 100644
index 0000000..1d7fcd5
--- /dev/null
+++ b/demo-mongodb/src/main/java/com/xkcoding/mongodb/model/Article.java
@@ -0,0 +1,60 @@
+package com.xkcoding.mongodb.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+
+import java.util.Date;
+
+/**
+ *
+ * 文章实体类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:21
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Article {
+ /**
+ * 文章id
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 文章标题
+ */
+ private String title;
+
+ /**
+ * 文章内容
+ */
+ private String content;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+ /**
+ * 更新时间
+ */
+ private Date updateTime;
+
+ /**
+ * 点赞数量
+ */
+ private Long thumbUp;
+
+ /**
+ * 访客数量
+ */
+ private Long visits;
+
+}
diff --git a/demo-mongodb/src/main/java/com/xkcoding/mongodb/repository/ArticleRepository.java b/demo-mongodb/src/main/java/com/xkcoding/mongodb/repository/ArticleRepository.java
new file mode 100644
index 0000000..341fd62
--- /dev/null
+++ b/demo-mongodb/src/main/java/com/xkcoding/mongodb/repository/ArticleRepository.java
@@ -0,0 +1,24 @@
+package com.xkcoding.mongodb.repository;
+
+import com.xkcoding.mongodb.model.Article;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+import java.util.List;
+
+/**
+ *
+ * 文章 Dao
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:30
+ */
+public interface ArticleRepository extends MongoRepository {
+ /**
+ * 根据标题模糊查询
+ *
+ * @param title 标题
+ * @return 满足条件的文章列表
+ */
+ List findByTitleLike(String title);
+}
diff --git a/spring-boot-demo-mongodb/src/main/resources/application.yml b/demo-mongodb/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-mongodb/src/main/resources/application.yml
rename to demo-mongodb/src/main/resources/application.yml
diff --git a/spring-boot-demo-mongodb/src/test/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplicationTests.java b/demo-mongodb/src/test/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplicationTests.java
similarity index 100%
rename from spring-boot-demo-mongodb/src/test/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplicationTests.java
rename to demo-mongodb/src/test/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplicationTests.java
diff --git a/demo-mongodb/src/test/java/com/xkcoding/mongodb/repository/ArticleRepositoryTest.java b/demo-mongodb/src/test/java/com/xkcoding/mongodb/repository/ArticleRepositoryTest.java
new file mode 100644
index 0000000..6617c73
--- /dev/null
+++ b/demo-mongodb/src/test/java/com/xkcoding/mongodb/repository/ArticleRepositoryTest.java
@@ -0,0 +1,142 @@
+package com.xkcoding.mongodb.repository;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.collect.Lists;
+import com.xkcoding.mongodb.SpringBootDemoMongodbApplicationTests;
+import com.xkcoding.mongodb.model.Article;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 测试操作 MongoDb
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-28 16:35
+ */
+@Slf4j
+public class ArticleRepositoryTest extends SpringBootDemoMongodbApplicationTests {
+ @Autowired
+ private ArticleRepository articleRepo;
+
+ @Autowired
+ private MongoTemplate mongoTemplate;
+
+ @Autowired
+ private Snowflake snowflake;
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void testSave() {
+ Article article = new Article(1L, RandomUtil.randomString(20), RandomUtil.randomString(150), DateUtil.date(), DateUtil.date(), 0L, 0L);
+ articleRepo.save(article);
+ log.info("【article】= {}", JSONUtil.toJsonStr(article));
+ }
+
+ /**
+ * 测试新增列表
+ */
+ @Test
+ public void testSaveList() {
+ List articles = Lists.newArrayList();
+ for (int i = 0; i < 10; i++) {
+ articles.add(new Article(snowflake.nextId(), RandomUtil.randomString(20), RandomUtil.randomString(150), DateUtil.date(), DateUtil.date(), 0L, 0L));
+ }
+ articleRepo.saveAll(articles);
+
+ log.info("【articles】= {}", JSONUtil.toJsonStr(articles.stream().map(Article::getId).collect(Collectors.toList())));
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void testUpdate() {
+ articleRepo.findById(1L).ifPresent(article -> {
+ article.setTitle(article.getTitle() + "更新之后的标题");
+ article.setUpdateTime(DateUtil.date());
+ articleRepo.save(article);
+ log.info("【article】= {}", JSONUtil.toJsonStr(article));
+ });
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void testDelete() {
+ // 根据主键删除
+ articleRepo.deleteById(1L);
+
+ // 全部删除
+ articleRepo.deleteAll();
+ }
+
+ /**
+ * 测试点赞数、访客数,使用save方式更新点赞、访客
+ */
+ @Test
+ public void testThumbUp() {
+ articleRepo.findById(1L).ifPresent(article -> {
+ article.setThumbUp(article.getThumbUp() + 1);
+ article.setVisits(article.getVisits() + 1);
+ articleRepo.save(article);
+ log.info("【标题】= {}【点赞数】= {}【访客数】= {}", article.getTitle(), article.getThumbUp(), article.getVisits());
+ });
+ }
+
+ /**
+ * 测试点赞数、访客数,使用更优雅/高效的方式更新点赞、访客
+ */
+ @Test
+ public void testThumbUp2() {
+ Query query = new Query();
+ query.addCriteria(Criteria.where("_id").is(1L));
+ Update update = new Update();
+ update.inc("thumbUp", 1L);
+ update.inc("visits", 1L);
+ mongoTemplate.updateFirst(query, update, "article");
+
+ articleRepo.findById(1L).ifPresent(article -> log.info("【标题】= {}【点赞数】= {}【访客数】= {}", article.getTitle(), article.getThumbUp(), article.getVisits()));
+ }
+
+ /**
+ * 测试分页排序查询
+ */
+ @Test
+ public void testQuery() {
+ Sort sort = Sort.by("thumbUp", "updateTime").descending();
+ PageRequest pageRequest = PageRequest.of(0, 5, sort);
+ Page all = articleRepo.findAll(pageRequest);
+ log.info("【总页数】= {}", all.getTotalPages());
+ log.info("【总条数】= {}", all.getTotalElements());
+ log.info("【当前页数据】= {}", JSONUtil.toJsonStr(all.getContent().stream().map(article -> "文章标题:" + article.getTitle() + "点赞数:" + article.getThumbUp() + "更新时间:" + article.getUpdateTime()).collect(Collectors.toList())));
+ }
+
+ /**
+ * 测试根据标题模糊查询
+ */
+ @Test
+ public void testFindByTitleLike() {
+ List articles = articleRepo.findByTitleLike("更新");
+ log.info("【articles】= {}", JSONUtil.toJsonStr(articles));
+ }
+
+}
diff --git a/spring-boot-demo-mq-kafka/.gitignore b/demo-mq-kafka/.gitignore
similarity index 100%
rename from spring-boot-demo-mq-kafka/.gitignore
rename to demo-mq-kafka/.gitignore
diff --git a/demo-mq-kafka/README.md b/demo-mq-kafka/README.md
new file mode 100644
index 0000000..55b684e
--- /dev/null
+++ b/demo-mq-kafka/README.md
@@ -0,0 +1,255 @@
+# spring-boot-demo-mq-kafka
+
+> 本 demo 主要演示了 Spring Boot 如何集成 kafka,实现消息的发送和接收。
+
+## 环境准备
+
+> 注意:本 demo 基于 Spring Boot 2.1.0.RELEASE 版本,因此 spring-kafka 的版本为 2.2.0.RELEASE,kafka-clients 的版本为2.0.0,所以 kafka 的版本选用为 kafka_2.11-2.1.0
+
+创建一个名为 `test` 的Topic
+
+```bash
+./bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
+```
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-mq-kafka
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-mq-kafka
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ spring-boot-demo-mq-kafka
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+server:
+ port: 8080
+ servlet:
+ context-path: /demo
+spring:
+ kafka:
+ bootstrap-servers: localhost:9092
+ producer:
+ retries: 0
+ batch-size: 16384
+ buffer-memory: 33554432
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.apache.kafka.common.serialization.StringSerializer
+ consumer:
+ group-id: spring-boot-demo
+ # 手动提交
+ enable-auto-commit: false
+ auto-offset-reset: latest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ properties:
+ session.timeout.ms: 60000
+ listener:
+ log-container-config: false
+ concurrency: 5
+ # 手动提交
+ ack-mode: manual_immediate
+```
+
+## KafkaConfig.java
+
+```java
+/**
+ *
+ * kafka配置类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-07 14:49
+ */
+@Configuration
+@EnableConfigurationProperties({KafkaProperties.class})
+@EnableKafka
+@AllArgsConstructor
+public class KafkaConfig {
+ private final KafkaProperties kafkaProperties;
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+
+ @Bean
+ public ProducerFactory producerFactory() {
+ return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+ factory.setBatchListener(true);
+ factory.getContainerProperties().setPollTimeout(3000);
+ return factory;
+ }
+
+ @Bean
+ public ConsumerFactory consumerFactory() {
+ return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
+ }
+
+ @Bean("ackContainerFactory")
+ public ConcurrentKafkaListenerContainerFactory ackContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
+ factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+ return factory;
+ }
+
+}
+```
+
+## MessageHandler.java
+
+```java
+/**
+ *
+ * 消息处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-07 14:58
+ */
+@Component
+@Slf4j
+public class MessageHandler {
+
+ @KafkaListener(topics = KafkaConsts.TOPIC_TEST, containerFactory = "ackContainerFactory")
+ public void handleMessage(ConsumerRecord record, Acknowledgment acknowledgment) {
+ try {
+ String message = (String) record.value();
+ log.info("收到消息: {}", message);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ // 手动提交 offset
+ acknowledgment.acknowledge();
+ }
+ }
+}
+```
+
+## SpringBootDemoMqKafkaApplicationTests.java
+
+```java
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoMqKafkaApplicationTests {
+ @Autowired
+ private KafkaTemplate kafkaTemplate;
+
+ /**
+ * 测试发送消息
+ */
+ @Test
+ public void testSend() {
+ kafkaTemplate.send(KafkaConsts.TOPIC_TEST, "hello,kafka...");
+ }
+
+}
+```
+
+## 参考
+
+1. Spring Boot 版本和 Spring-Kafka 的版本对应关系:https://spring.io/projects/spring-kafka
+
+ | Spring for Apache Kafka Version | Spring Integration for Apache Kafka Version | kafka-clients |
+ | ------------------------------- | ------------------------------------------- | ------------------- |
+ | 2.2.x | 3.1.x | 2.0.0, 2.1.0 |
+ | 2.1.x | 3.0.x | 1.0.x, 1.1.x, 2.0.0 |
+ | 2.0.x | 3.0.x | 0.11.0.x, 1.0.x |
+ | 1.3.x | 2.3.x | 0.11.0.x, 1.0.x |
+ | 1.2.x | 2.2.x | 0.10.2.x |
+ | 1.1.x | 2.1.x | 0.10.0.x, 0.10.1.x |
+ | 1.0.x | 2.0.x | 0.9.x.x |
+ | N/A* | 1.3.x | 0.8.2.2 |
+
+ > **IMPORTANT:** This matrix is client compatibility; in most cases (since 0.10.2.0) newer clients can communicate with older brokers. All users with brokers >= 0.10.x.x **(and all spring boot 1.5.x users)** are recommended to use spring-kafka version 1.3.x or higher due to its simpler threading model thanks to [KIP-62](https://cwiki.apache.org/confluence/display/KAFKA/KIP-62%3A+Allow+consumer+to+send+heartbeats+from+a+background+thread). For a complete discussion about client/broker compatibility, see the Kafka [Compatibility Matrix](https://cwiki.apache.org/confluence/display/KAFKA/Compatibility+Matrix)
+ >
+ > - Spring Integration Kafka versions prior to 2.0 pre-dated the Spring for Apache Kafka project and therefore were not based on it.
+ >
+ > These versions will be referenced transitively when using maven or gradle for version management. For the 1.1.x version, the 0.10.1.x is the default.
+ >
+ > 2.1.x uses the 1.1.x kafka-clients by default. When overriding the kafka-clients for 2.1.x see [the documentation appendix](https://docs.spring.io/spring-kafka/docs/2.1.x/reference/html/deps-for-11x.html).
+ >
+ > 2.2.x uses the 2.0.x kafka-clients by default. When overriding the kafka-clients for 2.2.x see [the documentation appendix](https://docs.spring.io/spring-kafka/docs/2.2.1.BUILD-SNAPSHOT/reference/html/deps-for-21x.html).
+ >
+ > - Spring Boot 1.5 users should use 1.3.x (Boot dependency management will use 1.1.x by default so this should be overridden).
+ > - Spring Boot 2.0 users should use 2.0.x (Boot dependency management will use the correct version).
+ > - Spring Boot 2.1 users should use 2.2.x (Boot dependency management will use the correct version).
+
+2. Spring-Kafka 官方文档:https://docs.spring.io/spring-kafka/docs/2.2.0.RELEASE/reference/html/
diff --git a/demo-mq-kafka/pom.xml b/demo-mq-kafka/pom.xml
new file mode 100644
index 0000000..6168aa9
--- /dev/null
+++ b/demo-mq-kafka/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ demo-mq-kafka
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-mq-kafka
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ demo-mq-kafka
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplication.java b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplication.java
new file mode 100644
index 0000000..c2e8e5e
--- /dev/null
+++ b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.mq.kafka;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-07 14:43
+ */
+@SpringBootApplication
+public class SpringBootDemoMqKafkaApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoMqKafkaApplication.class, args);
+ }
+
+}
+
diff --git a/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/config/KafkaConfig.java b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/config/KafkaConfig.java
new file mode 100644
index 0000000..b7d9c75
--- /dev/null
+++ b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/config/KafkaConfig.java
@@ -0,0 +1,63 @@
+package com.xkcoding.mq.kafka.config;
+
+import com.xkcoding.mq.kafka.constants.KafkaConsts;
+import lombok.AllArgsConstructor;
+import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.*;
+import org.springframework.kafka.listener.ContainerProperties;
+
+/**
+ *
+ * kafka配置类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-07 14:49
+ */
+@Configuration
+@EnableConfigurationProperties({KafkaProperties.class})
+@EnableKafka
+@AllArgsConstructor
+public class KafkaConfig {
+ private final KafkaProperties kafkaProperties;
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+
+ @Bean
+ public ProducerFactory producerFactory() {
+ return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+ factory.setBatchListener(true);
+ factory.getContainerProperties().setPollTimeout(3000);
+ return factory;
+ }
+
+ @Bean
+ public ConsumerFactory consumerFactory() {
+ return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
+ }
+
+ @Bean("ackContainerFactory")
+ public ConcurrentKafkaListenerContainerFactory ackContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
+ factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+ return factory;
+ }
+
+}
diff --git a/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/constants/KafkaConsts.java b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/constants/KafkaConsts.java
new file mode 100644
index 0000000..3546abb
--- /dev/null
+++ b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/constants/KafkaConsts.java
@@ -0,0 +1,21 @@
+package com.xkcoding.mq.kafka.constants;
+
+/**
+ *
+ * kafka 常量池
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-07 14:52
+ */
+public interface KafkaConsts {
+ /**
+ * 默认分区大小
+ */
+ Integer DEFAULT_PARTITION_NUM = 3;
+
+ /**
+ * Topic 名称
+ */
+ String TOPIC_TEST = "test";
+}
diff --git a/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/handler/MessageHandler.java b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/handler/MessageHandler.java
new file mode 100644
index 0000000..61dee17
--- /dev/null
+++ b/demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/handler/MessageHandler.java
@@ -0,0 +1,34 @@
+package com.xkcoding.mq.kafka.handler;
+
+import com.xkcoding.mq.kafka.constants.KafkaConsts;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.support.Acknowledgment;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * 消息处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-07 14:58
+ */
+@Component
+@Slf4j
+public class MessageHandler {
+
+ @KafkaListener(topics = KafkaConsts.TOPIC_TEST, containerFactory = "ackContainerFactory")
+ public void handleMessage(ConsumerRecord record, Acknowledgment acknowledgment) {
+ try {
+ String message = (String) record.value();
+ log.info("收到消息: {}", message);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ // 手动提交 offset
+ acknowledgment.acknowledge();
+ }
+ }
+}
diff --git a/spring-boot-demo-mq-kafka/src/main/resources/application.yml b/demo-mq-kafka/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-mq-kafka/src/main/resources/application.yml
rename to demo-mq-kafka/src/main/resources/application.yml
diff --git a/spring-boot-demo-mq-kafka/src/test/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplicationTests.java b/demo-mq-kafka/src/test/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplicationTests.java
similarity index 100%
rename from spring-boot-demo-mq-kafka/src/test/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplicationTests.java
rename to demo-mq-kafka/src/test/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplicationTests.java
diff --git a/spring-boot-demo-mq-rabbitmq/.gitignore b/demo-mq-rabbitmq/.gitignore
similarity index 100%
rename from spring-boot-demo-mq-rabbitmq/.gitignore
rename to demo-mq-rabbitmq/.gitignore
diff --git a/demo-mq-rabbitmq/README.md b/demo-mq-rabbitmq/README.md
new file mode 100644
index 0000000..d7fe45e
--- /dev/null
+++ b/demo-mq-rabbitmq/README.md
@@ -0,0 +1,534 @@
+# spring-boot-demo-mq-rabbitmq
+
+> 此 demo 主要演示了 Spring Boot 如何集成 RabbitMQ,并且演示了基于直接队列模式、分列模式、主题模式、延迟队列的消息发送和接收。
+
+## 注意
+
+作者编写本demo时,RabbitMQ 版本使用 `3.7.7-management`,使用 docker 运行,下面是所有步骤:
+
+1. 下载镜像:`docker pull rabbitmq:3.7.7-management`
+
+2. 运行容器:`docker run -d -p 5671:5617 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 --name rabbit-3.7.7 rabbitmq:3.7.7-management`
+
+3. 进入容器:`docker exec -it rabbit-3.7.7 /bin/bash`
+
+4. 给容器安装 下载工具 wget:`apt-get install -y wget`
+
+5. 下载插件包,因为我们的 `RabbitMQ` 版本为 `3.7.7` 所以我们安装 `3.7.x` 版本的延迟队列插件
+
+ ```bash
+ root@f72ac937f2be:/plugins# wget https://dl.bintray.com/rabbitmq/community-plugins/3.7.x/rabbitmq_delayed_message_exchange/rabbitmq_delayed_message_exchange-20171201-3.7.x.zip
+ ```
+
+6. 给容器安装 解压工具 unzip:`apt-get install -y unzip`
+
+7. 解压插件包
+
+ ```bash
+ root@f72ac937f2be:/plugins# unzip rabbitmq_delayed_message_exchange-20171201-3.7.x.zip
+ Archive: rabbitmq_delayed_message_exchange-20171201-3.7.x.zip
+ inflating: rabbitmq_delayed_message_exchange-20171201-3.7.x.ez
+ ```
+
+8. 启动延迟队列插件
+
+ ```yaml
+ root@f72ac937f2be:/plugins# rabbitmq-plugins enable rabbitmq_delayed_message_exchange
+ The following plugins have been configured:
+ rabbitmq_delayed_message_exchange
+ rabbitmq_management
+ rabbitmq_management_agent
+ rabbitmq_web_dispatch
+ Applying plugin configuration to rabbit@f72ac937f2be...
+ The following plugins have been enabled:
+ rabbitmq_delayed_message_exchange
+
+ started 1 plugins.
+ ```
+
+9. 退出容器:`exit`
+
+10. 停止容器:`docker stop rabbit-3.7.7`
+
+11. 启动容器:`docker start rabbit-3.7.7`
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-mq-rabbitmq
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-mq-rabbitmq
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ spring-boot-demo-mq-rabbitmq
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+server:
+ port: 8080
+ servlet:
+ context-path: /demo
+spring:
+ rabbitmq:
+ host: localhost
+ port: 5672
+ username: guest
+ password: guest
+ virtual-host: /
+ # 手动提交消息
+ listener:
+ simple:
+ acknowledge-mode: manual
+ direct:
+ acknowledge-mode: manual
+```
+
+## RabbitConsts.java
+
+```java
+/**
+ *
+ * RabbitMQ常量池
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 17:08
+ */
+public interface RabbitConsts {
+ /**
+ * 直接模式1
+ */
+ String DIRECT_MODE_QUEUE_ONE = "queue.direct.1";
+
+ /**
+ * 队列2
+ */
+ String QUEUE_TWO = "queue.2";
+
+ /**
+ * 队列3
+ */
+ String QUEUE_THREE = "3.queue";
+
+ /**
+ * 分列模式
+ */
+ String FANOUT_MODE_QUEUE = "fanout.mode";
+
+ /**
+ * 主题模式
+ */
+ String TOPIC_MODE_QUEUE = "topic.mode";
+
+ /**
+ * 路由1
+ */
+ String TOPIC_ROUTING_KEY_ONE = "queue.#";
+
+ /**
+ * 路由2
+ */
+ String TOPIC_ROUTING_KEY_TWO = "*.queue";
+
+ /**
+ * 路由3
+ */
+ String TOPIC_ROUTING_KEY_THREE = "3.queue";
+
+ /**
+ * 延迟队列
+ */
+ String DELAY_QUEUE = "delay.queue";
+
+ /**
+ * 延迟队列交换器
+ */
+ String DELAY_MODE_QUEUE = "delay.mode";
+}
+```
+
+## RabbitMqConfig.java
+
+> RoutingKey规则
+>
+> - 路由格式必须以 `.` 分隔,比如 `user.email` 或者 `user.aaa.email`
+> - 通配符 `*` ,代表一个占位符,或者说一个单词,比如路由为 `user.*`,那么 **`user.email`** 可以匹配,但是 *`user.aaa.email`* 就匹配不了
+> - 通配符 `#` ,代表一个或多个占位符,或者说一个或多个单词,比如路由为 `user.#`,那么 **`user.email`** 可以匹配,**`user.aaa.email `** 也可以匹配
+
+```java
+/**
+ *
+ * RabbitMQ配置,主要是配置队列,如果提前存在该队列,可以省略本配置类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 17:03
+ */
+@Slf4j
+@Configuration
+public class RabbitMqConfig {
+
+ @Bean
+ public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
+ connectionFactory.setPublisherConfirms(true);
+ connectionFactory.setPublisherReturns(true);
+ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
+ rabbitTemplate.setMandatory(true);
+ rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
+ rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
+ return rabbitTemplate;
+ }
+
+ /**
+ * 直接模式队列1
+ */
+ @Bean
+ public Queue directOneQueue() {
+ return new Queue(RabbitConsts.DIRECT_MODE_QUEUE_ONE);
+ }
+
+ /**
+ * 队列2
+ */
+ @Bean
+ public Queue queueTwo() {
+ return new Queue(RabbitConsts.QUEUE_TWO);
+ }
+
+ /**
+ * 队列3
+ */
+ @Bean
+ public Queue queueThree() {
+ return new Queue(RabbitConsts.QUEUE_THREE);
+ }
+
+ /**
+ * 分列模式队列
+ */
+ @Bean
+ public FanoutExchange fanoutExchange() {
+ return new FanoutExchange(RabbitConsts.FANOUT_MODE_QUEUE);
+ }
+
+ /**
+ * 分列模式绑定队列1
+ *
+ * @param directOneQueue 绑定队列1
+ * @param fanoutExchange 分列模式交换器
+ */
+ @Bean
+ public Binding fanoutBinding1(Queue directOneQueue, FanoutExchange fanoutExchange) {
+ return BindingBuilder.bind(directOneQueue).to(fanoutExchange);
+ }
+
+ /**
+ * 分列模式绑定队列2
+ *
+ * @param queueTwo 绑定队列2
+ * @param fanoutExchange 分列模式交换器
+ */
+ @Bean
+ public Binding fanoutBinding2(Queue queueTwo, FanoutExchange fanoutExchange) {
+ return BindingBuilder.bind(queueTwo).to(fanoutExchange);
+ }
+
+ /**
+ * 主题模式队列
+ * 路由格式必须以 . 分隔,比如 user.email 或者 user.aaa.email
+ * 通配符 * ,代表一个占位符,或者说一个单词,比如路由为 user.*,那么 user.email 可以匹配,但是 user.aaa.email 就匹配不了
+ * 通配符 # ,代表一个或多个占位符,或者说一个或多个单词,比如路由为 user.#,那么 user.email 可以匹配,user.aaa.email 也可以匹配
+ */
+ @Bean
+ public TopicExchange topicExchange() {
+ return new TopicExchange(RabbitConsts.TOPIC_MODE_QUEUE);
+ }
+
+
+ /**
+ * 主题模式绑定分列模式
+ *
+ * @param fanoutExchange 分列模式交换器
+ * @param topicExchange 主题模式交换器
+ */
+ @Bean
+ public Binding topicBinding1(FanoutExchange fanoutExchange, TopicExchange topicExchange) {
+ return BindingBuilder.bind(fanoutExchange).to(topicExchange).with(RabbitConsts.TOPIC_ROUTING_KEY_ONE);
+ }
+
+ /**
+ * 主题模式绑定队列2
+ *
+ * @param queueTwo 队列2
+ * @param topicExchange 主题模式交换器
+ */
+ @Bean
+ public Binding topicBinding2(Queue queueTwo, TopicExchange topicExchange) {
+ return BindingBuilder.bind(queueTwo).to(topicExchange).with(RabbitConsts.TOPIC_ROUTING_KEY_TWO);
+ }
+
+ /**
+ * 主题模式绑定队列3
+ *
+ * @param queueThree 队列3
+ * @param topicExchange 主题模式交换器
+ */
+ @Bean
+ public Binding topicBinding3(Queue queueThree, TopicExchange topicExchange) {
+ return BindingBuilder.bind(queueThree).to(topicExchange).with(RabbitConsts.TOPIC_ROUTING_KEY_THREE);
+ }
+
+ /**
+ * 延迟队列
+ */
+ @Bean
+ public Queue delayQueue() {
+ return new Queue(RabbitConsts.DELAY_QUEUE, true);
+ }
+
+ /**
+ * 延迟队列交换器, x-delayed-type 和 x-delayed-message 固定
+ */
+ @Bean
+ public CustomExchange delayExchange() {
+ Map args = Maps.newHashMap();
+ args.put("x-delayed-type", "direct");
+ return new CustomExchange(RabbitConsts.DELAY_MODE_QUEUE, "x-delayed-message", true, false, args);
+ }
+
+ /**
+ * 延迟队列绑定自定义交换器
+ *
+ * @param delayQueue 队列
+ * @param delayExchange 延迟交换器
+ */
+ @Bean
+ public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
+ return BindingBuilder.bind(delayQueue).to(delayExchange).with(RabbitConsts.DELAY_QUEUE).noargs();
+ }
+
+}
+```
+
+## 消息处理器
+
+> 只展示直接队列模式的消息处理,其余模式请看源码
+>
+> 需要注意:如果 `spring.rabbitmq.listener.direct.acknowledge-mode: auto`,则会自动Ack,否则需要手动Ack
+
+### DirectQueueOneHandler.java
+
+```java
+/**
+ *
+ * 直接队列1 处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-04 15:42
+ */
+@Slf4j
+@RabbitListener(queues = RabbitConsts.DIRECT_MODE_QUEUE_ONE)
+@Component
+public class DirectQueueOneHandler {
+
+ /**
+ * 如果 spring.rabbitmq.listener.direct.acknowledge-mode: auto,则可以用这个方式,会自动ack
+ */
+ // @RabbitHandler
+ public void directHandlerAutoAck(MessageStruct message) {
+ log.info("直接队列处理器,接收消息:{}", JSONUtil.toJsonStr(message));
+ }
+
+ @RabbitHandler
+ public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
+ // 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
+ final long deliveryTag = message.getMessageProperties().getDeliveryTag();
+ try {
+ log.info("直接队列1,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
+ // 通知 MQ 消息已被成功消费,可以ACK了
+ channel.basicAck(deliveryTag, false);
+ } catch (IOException e) {
+ try {
+ // 处理失败,重新压入MQ
+ channel.basicRecover();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+}
+```
+
+## SpringBootDemoMqRabbitmqApplicationTests.java
+
+```java
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoMqRabbitmqApplicationTests {
+ @Autowired
+ private RabbitTemplate rabbitTemplate;
+
+ /**
+ * 测试直接模式发送
+ */
+ @Test
+ public void sendDirect() {
+ rabbitTemplate.convertAndSend(RabbitConsts.DIRECT_MODE_QUEUE_ONE, new MessageStruct("direct message"));
+ }
+
+ /**
+ * 测试分列模式发送
+ */
+ @Test
+ public void sendFanout() {
+ rabbitTemplate.convertAndSend(RabbitConsts.FANOUT_MODE_QUEUE, "", new MessageStruct("fanout message"));
+ }
+
+ /**
+ * 测试主题模式发送1
+ */
+ @Test
+ public void sendTopic1() {
+ rabbitTemplate.convertAndSend(RabbitConsts.TOPIC_MODE_QUEUE, "queue.aaa.bbb", new MessageStruct("topic message"));
+ }
+
+ /**
+ * 测试主题模式发送2
+ */
+ @Test
+ public void sendTopic2() {
+ rabbitTemplate.convertAndSend(RabbitConsts.TOPIC_MODE_QUEUE, "ccc.queue", new MessageStruct("topic message"));
+ }
+
+ /**
+ * 测试主题模式发送3
+ */
+ @Test
+ public void sendTopic3() {
+ rabbitTemplate.convertAndSend(RabbitConsts.TOPIC_MODE_QUEUE, "3.queue", new MessageStruct("topic message"));
+ }
+
+ /**
+ * 测试延迟队列发送
+ */
+ @Test
+ public void sendDelay() {
+ rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 5s, " + DateUtil
+ .date()), message -> {
+ message.getMessageProperties().setHeader("x-delay", 5000);
+ return message;
+ });
+ rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 2s, " + DateUtil
+ .date()), message -> {
+ message.getMessageProperties().setHeader("x-delay", 2000);
+ return message;
+ });
+ rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 8s, " + DateUtil
+ .date()), message -> {
+ message.getMessageProperties().setHeader("x-delay", 8000);
+ return message;
+ });
+ }
+
+}
+```
+
+## 运行效果
+
+### 直接模式
+
+![image-20190107103229408](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063315-1.jpg)
+
+### 分列模式
+
+![image-20190107103258291](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063315.jpg)
+
+### 主题模式
+
+#### RoutingKey:`queue.#`
+
+![image-20190107103358744](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063316.jpg)
+
+#### RoutingKey:`*.queue`
+
+![image-20190107103429430](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063312.jpg)
+
+#### RoutingKey:`3.queue`
+
+![image-20190107103451240](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063313.jpg)
+
+### 延迟队列
+
+![image-20190107103509943](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063314.jpg)
+
+## 参考
+
+1. SpringQP 官方文档:https://docs.spring.io/spring-amqp/docs/2.1.0.RELEASE/reference/html/
+2. RabbitMQ 官网:http://www.rabbitmq.com/
+3. RabbitMQ延迟队列:https://www.cnblogs.com/vipstone/p/9967649.html
diff --git a/demo-mq-rabbitmq/pom.xml b/demo-mq-rabbitmq/pom.xml
new file mode 100644
index 0000000..fcb1f28
--- /dev/null
+++ b/demo-mq-rabbitmq/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ demo-mq-rabbitmq
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-mq-rabbitmq
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ demo-mq-rabbitmq
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplication.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplication.java
new file mode 100644
index 0000000..2d96bb9
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.mq.rabbitmq;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 13:58
+ */
+@SpringBootApplication
+public class SpringBootDemoMqRabbitmqApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoMqRabbitmqApplication.class, args);
+ }
+
+}
+
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/config/RabbitMqConfig.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/config/RabbitMqConfig.java
new file mode 100644
index 0000000..77addf0
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/config/RabbitMqConfig.java
@@ -0,0 +1,165 @@
+package com.xkcoding.mq.rabbitmq.config;
+
+import com.google.common.collect.Maps;
+import com.xkcoding.mq.rabbitmq.constants.RabbitConsts;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.*;
+import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Map;
+
+/**
+ *
+ * RabbitMQ配置,主要是配置队列,如果提前存在该队列,可以省略本配置类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 17:03
+ */
+@Slf4j
+@Configuration
+public class RabbitMqConfig {
+
+ @Bean
+ public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
+ connectionFactory.setPublisherConfirms(true);
+ connectionFactory.setPublisherReturns(true);
+ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
+ rabbitTemplate.setMandatory(true);
+ rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
+ rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
+ return rabbitTemplate;
+ }
+
+ /**
+ * 直接模式队列1
+ */
+ @Bean
+ public Queue directOneQueue() {
+ return new Queue(RabbitConsts.DIRECT_MODE_QUEUE_ONE);
+ }
+
+ /**
+ * 队列2
+ */
+ @Bean
+ public Queue queueTwo() {
+ return new Queue(RabbitConsts.QUEUE_TWO);
+ }
+
+ /**
+ * 队列3
+ */
+ @Bean
+ public Queue queueThree() {
+ return new Queue(RabbitConsts.QUEUE_THREE);
+ }
+
+ /**
+ * 分列模式队列
+ */
+ @Bean
+ public FanoutExchange fanoutExchange() {
+ return new FanoutExchange(RabbitConsts.FANOUT_MODE_QUEUE);
+ }
+
+ /**
+ * 分列模式绑定队列1
+ *
+ * @param directOneQueue 绑定队列1
+ * @param fanoutExchange 分列模式交换器
+ */
+ @Bean
+ public Binding fanoutBinding1(Queue directOneQueue, FanoutExchange fanoutExchange) {
+ return BindingBuilder.bind(directOneQueue).to(fanoutExchange);
+ }
+
+ /**
+ * 分列模式绑定队列2
+ *
+ * @param queueTwo 绑定队列2
+ * @param fanoutExchange 分列模式交换器
+ */
+ @Bean
+ public Binding fanoutBinding2(Queue queueTwo, FanoutExchange fanoutExchange) {
+ return BindingBuilder.bind(queueTwo).to(fanoutExchange);
+ }
+
+ /**
+ * 主题模式队列
+ * 路由格式必须以 . 分隔,比如 user.email 或者 user.aaa.email
+ * 通配符 * ,代表一个占位符,或者说一个单词,比如路由为 user.*,那么 user.email 可以匹配,但是 user.aaa.email 就匹配不了
+ * 通配符 # ,代表一个或多个占位符,或者说一个或多个单词,比如路由为 user.#,那么 user.email 可以匹配,user.aaa.email 也可以匹配
+ */
+ @Bean
+ public TopicExchange topicExchange() {
+ return new TopicExchange(RabbitConsts.TOPIC_MODE_QUEUE);
+ }
+
+
+ /**
+ * 主题模式绑定分列模式
+ *
+ * @param fanoutExchange 分列模式交换器
+ * @param topicExchange 主题模式交换器
+ */
+ @Bean
+ public Binding topicBinding1(FanoutExchange fanoutExchange, TopicExchange topicExchange) {
+ return BindingBuilder.bind(fanoutExchange).to(topicExchange).with(RabbitConsts.TOPIC_ROUTING_KEY_ONE);
+ }
+
+ /**
+ * 主题模式绑定队列2
+ *
+ * @param queueTwo 队列2
+ * @param topicExchange 主题模式交换器
+ */
+ @Bean
+ public Binding topicBinding2(Queue queueTwo, TopicExchange topicExchange) {
+ return BindingBuilder.bind(queueTwo).to(topicExchange).with(RabbitConsts.TOPIC_ROUTING_KEY_TWO);
+ }
+
+ /**
+ * 主题模式绑定队列3
+ *
+ * @param queueThree 队列3
+ * @param topicExchange 主题模式交换器
+ */
+ @Bean
+ public Binding topicBinding3(Queue queueThree, TopicExchange topicExchange) {
+ return BindingBuilder.bind(queueThree).to(topicExchange).with(RabbitConsts.TOPIC_ROUTING_KEY_THREE);
+ }
+
+ /**
+ * 延迟队列
+ */
+ @Bean
+ public Queue delayQueue() {
+ return new Queue(RabbitConsts.DELAY_QUEUE, true);
+ }
+
+ /**
+ * 延迟队列交换器, x-delayed-type 和 x-delayed-message 固定
+ */
+ @Bean
+ public CustomExchange delayExchange() {
+ Map args = Maps.newHashMap();
+ args.put("x-delayed-type", "direct");
+ return new CustomExchange(RabbitConsts.DELAY_MODE_QUEUE, "x-delayed-message", true, false, args);
+ }
+
+ /**
+ * 延迟队列绑定自定义交换器
+ *
+ * @param delayQueue 队列
+ * @param delayExchange 延迟交换器
+ */
+ @Bean
+ public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
+ return BindingBuilder.bind(delayQueue).to(delayExchange).with(RabbitConsts.DELAY_QUEUE).noargs();
+ }
+
+}
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/constants/RabbitConsts.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/constants/RabbitConsts.java
new file mode 100644
index 0000000..7516746
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/constants/RabbitConsts.java
@@ -0,0 +1,61 @@
+package com.xkcoding.mq.rabbitmq.constants;
+
+/**
+ *
+ * RabbitMQ常量池
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 17:08
+ */
+public interface RabbitConsts {
+ /**
+ * 直接模式1
+ */
+ String DIRECT_MODE_QUEUE_ONE = "queue.direct.1";
+
+ /**
+ * 队列2
+ */
+ String QUEUE_TWO = "queue.2";
+
+ /**
+ * 队列3
+ */
+ String QUEUE_THREE = "3.queue";
+
+ /**
+ * 分列模式
+ */
+ String FANOUT_MODE_QUEUE = "fanout.mode";
+
+ /**
+ * 主题模式
+ */
+ String TOPIC_MODE_QUEUE = "topic.mode";
+
+ /**
+ * 路由1
+ */
+ String TOPIC_ROUTING_KEY_ONE = "queue.#";
+
+ /**
+ * 路由2
+ */
+ String TOPIC_ROUTING_KEY_TWO = "*.queue";
+
+ /**
+ * 路由3
+ */
+ String TOPIC_ROUTING_KEY_THREE = "3.queue";
+
+ /**
+ * 延迟队列
+ */
+ String DELAY_QUEUE = "delay.queue";
+
+ /**
+ * 延迟队列交换器
+ */
+ String DELAY_MODE_QUEUE = "delay.mode";
+}
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DelayQueueHandler.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DelayQueueHandler.java
new file mode 100644
index 0000000..15f4b24
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DelayQueueHandler.java
@@ -0,0 +1,45 @@
+package com.xkcoding.mq.rabbitmq.handler;
+
+import cn.hutool.json.JSONUtil;
+import com.rabbitmq.client.Channel;
+import com.xkcoding.mq.rabbitmq.constants.RabbitConsts;
+import com.xkcoding.mq.rabbitmq.message.MessageStruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ *
+ * 延迟队列处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-04 17:42
+ */
+@Slf4j
+@Component
+@RabbitListener(queues = RabbitConsts.DELAY_QUEUE)
+public class DelayQueueHandler {
+
+ @RabbitHandler
+ public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
+ // 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
+ final long deliveryTag = message.getMessageProperties().getDeliveryTag();
+ try {
+ log.info("延迟队列,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
+ // 通知 MQ 消息已被成功消费,可以ACK了
+ channel.basicAck(deliveryTag, false);
+ } catch (IOException e) {
+ try {
+ // 处理失败,重新压入MQ
+ channel.basicRecover();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DirectQueueOneHandler.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DirectQueueOneHandler.java
new file mode 100644
index 0000000..5b7559e
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DirectQueueOneHandler.java
@@ -0,0 +1,53 @@
+package com.xkcoding.mq.rabbitmq.handler;
+
+import cn.hutool.json.JSONUtil;
+import com.rabbitmq.client.Channel;
+import com.xkcoding.mq.rabbitmq.constants.RabbitConsts;
+import com.xkcoding.mq.rabbitmq.message.MessageStruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ *
+ * 直接队列1 处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-04 15:42
+ */
+@Slf4j
+@RabbitListener(queues = RabbitConsts.DIRECT_MODE_QUEUE_ONE)
+@Component
+public class DirectQueueOneHandler {
+
+ /**
+ * 如果 spring.rabbitmq.listener.direct.acknowledge-mode: auto,则可以用这个方式,会自动ack
+ */
+ // @RabbitHandler
+ public void directHandlerAutoAck(MessageStruct message) {
+ log.info("直接队列处理器,接收消息:{}", JSONUtil.toJsonStr(message));
+ }
+
+ @RabbitHandler
+ public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
+ // 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
+ final long deliveryTag = message.getMessageProperties().getDeliveryTag();
+ try {
+ log.info("直接队列1,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
+ // 通知 MQ 消息已被成功消费,可以ACK了
+ channel.basicAck(deliveryTag, false);
+ } catch (IOException e) {
+ try {
+ // 处理失败,重新压入MQ
+ channel.basicRecover();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueThreeHandler.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueThreeHandler.java
new file mode 100644
index 0000000..af229c1
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueThreeHandler.java
@@ -0,0 +1,45 @@
+package com.xkcoding.mq.rabbitmq.handler;
+
+import cn.hutool.json.JSONUtil;
+import com.rabbitmq.client.Channel;
+import com.xkcoding.mq.rabbitmq.constants.RabbitConsts;
+import com.xkcoding.mq.rabbitmq.message.MessageStruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ *
+ * 队列2 处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-04 15:42
+ */
+@Slf4j
+@RabbitListener(queues = RabbitConsts.QUEUE_THREE)
+@Component
+public class QueueThreeHandler {
+
+ @RabbitHandler
+ public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
+ // 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
+ final long deliveryTag = message.getMessageProperties().getDeliveryTag();
+ try {
+ log.info("队列3,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
+ // 通知 MQ 消息已被成功消费,可以ACK了
+ channel.basicAck(deliveryTag, false);
+ } catch (IOException e) {
+ try {
+ // 处理失败,重新压入MQ
+ channel.basicRecover();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueTwoHandler.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueTwoHandler.java
new file mode 100644
index 0000000..1369ab6
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueTwoHandler.java
@@ -0,0 +1,45 @@
+package com.xkcoding.mq.rabbitmq.handler;
+
+import cn.hutool.json.JSONUtil;
+import com.rabbitmq.client.Channel;
+import com.xkcoding.mq.rabbitmq.constants.RabbitConsts;
+import com.xkcoding.mq.rabbitmq.message.MessageStruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ *
+ * 队列2 处理器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-04 15:42
+ */
+@Slf4j
+@RabbitListener(queues = RabbitConsts.QUEUE_TWO)
+@Component
+public class QueueTwoHandler {
+
+ @RabbitHandler
+ public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
+ // 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
+ final long deliveryTag = message.getMessageProperties().getDeliveryTag();
+ try {
+ log.info("队列2,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
+ // 通知 MQ 消息已被成功消费,可以ACK了
+ channel.basicAck(deliveryTag, false);
+ } catch (IOException e) {
+ try {
+ // 处理失败,重新压入MQ
+ channel.basicRecover();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/message/MessageStruct.java b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/message/MessageStruct.java
new file mode 100644
index 0000000..71c1125
--- /dev/null
+++ b/demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/message/MessageStruct.java
@@ -0,0 +1,26 @@
+package com.xkcoding.mq.rabbitmq.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * 测试消息体
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-29 16:22
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MessageStruct implements Serializable {
+ private static final long serialVersionUID = 392365881428311040L;
+
+ private String message;
+}
diff --git a/spring-boot-demo-mq-rabbitmq/src/main/resources/application.yml b/demo-mq-rabbitmq/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-mq-rabbitmq/src/main/resources/application.yml
rename to demo-mq-rabbitmq/src/main/resources/application.yml
diff --git a/demo-mq-rabbitmq/src/test/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplicationTests.java b/demo-mq-rabbitmq/src/test/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplicationTests.java
new file mode 100644
index 0000000..27251cc
--- /dev/null
+++ b/demo-mq-rabbitmq/src/test/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplicationTests.java
@@ -0,0 +1,79 @@
+package com.xkcoding.mq.rabbitmq;
+
+import cn.hutool.core.date.DateUtil;
+import com.xkcoding.mq.rabbitmq.constants.RabbitConsts;
+import com.xkcoding.mq.rabbitmq.message.MessageStruct;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoMqRabbitmqApplicationTests {
+ @Autowired
+ private RabbitTemplate rabbitTemplate;
+
+ /**
+ * 测试直接模式发送
+ */
+ @Test
+ public void sendDirect() {
+ rabbitTemplate.convertAndSend(RabbitConsts.DIRECT_MODE_QUEUE_ONE, new MessageStruct("direct message"));
+ }
+
+ /**
+ * 测试分列模式发送
+ */
+ @Test
+ public void sendFanout() {
+ rabbitTemplate.convertAndSend(RabbitConsts.FANOUT_MODE_QUEUE, "", new MessageStruct("fanout message"));
+ }
+
+ /**
+ * 测试主题模式发送1
+ */
+ @Test
+ public void sendTopic1() {
+ rabbitTemplate.convertAndSend(RabbitConsts.TOPIC_MODE_QUEUE, "queue.aaa.bbb", new MessageStruct("topic message"));
+ }
+
+ /**
+ * 测试主题模式发送2
+ */
+ @Test
+ public void sendTopic2() {
+ rabbitTemplate.convertAndSend(RabbitConsts.TOPIC_MODE_QUEUE, "ccc.queue", new MessageStruct("topic message"));
+ }
+
+ /**
+ * 测试主题模式发送3
+ */
+ @Test
+ public void sendTopic3() {
+ rabbitTemplate.convertAndSend(RabbitConsts.TOPIC_MODE_QUEUE, "3.queue", new MessageStruct("topic message"));
+ }
+
+ /**
+ * 测试延迟队列发送
+ */
+ @Test
+ public void sendDelay() {
+ rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 5s, " + DateUtil.date()), message -> {
+ message.getMessageProperties().setHeader("x-delay", 5000);
+ return message;
+ });
+ rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 2s, " + DateUtil.date()), message -> {
+ message.getMessageProperties().setHeader("x-delay", 2000);
+ return message;
+ });
+ rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 8s, " + DateUtil.date()), message -> {
+ message.getMessageProperties().setHeader("x-delay", 8000);
+ return message;
+ });
+ }
+
+}
+
diff --git a/spring-boot-demo-mq-rocketmq/.gitignore b/demo-mq-rocketmq/.gitignore
similarity index 100%
rename from spring-boot-demo-mq-rocketmq/.gitignore
rename to demo-mq-rocketmq/.gitignore
diff --git a/spring-boot-demo-mq-rocketmq/README.md b/demo-mq-rocketmq/README.md
similarity index 100%
rename from spring-boot-demo-mq-rocketmq/README.md
rename to demo-mq-rocketmq/README.md
diff --git a/demo-mq-rocketmq/pom.xml b/demo-mq-rocketmq/pom.xml
new file mode 100644
index 0000000..0369f8f
--- /dev/null
+++ b/demo-mq-rocketmq/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+ demo-mq-rocketmq
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-mq-rocketmq
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ demo-mq-rocketmq
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/spring-boot-demo-mq-rocketmq/src/main/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplication.java b/demo-mq-rocketmq/src/main/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplication.java
similarity index 100%
rename from spring-boot-demo-mq-rocketmq/src/main/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplication.java
rename to demo-mq-rocketmq/src/main/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplication.java
diff --git a/spring-boot-demo-mq-rocketmq/src/main/resources/application.properties b/demo-mq-rocketmq/src/main/resources/application.properties
similarity index 100%
rename from spring-boot-demo-mq-rocketmq/src/main/resources/application.properties
rename to demo-mq-rocketmq/src/main/resources/application.properties
diff --git a/spring-boot-demo-mq-rocketmq/src/test/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplicationTests.java b/demo-mq-rocketmq/src/test/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplicationTests.java
similarity index 100%
rename from spring-boot-demo-mq-rocketmq/src/test/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplicationTests.java
rename to demo-mq-rocketmq/src/test/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplicationTests.java
diff --git a/spring-boot-demo-multi-datasource-jpa/.gitignore b/demo-multi-datasource-jpa/.gitignore
similarity index 100%
rename from spring-boot-demo-multi-datasource-jpa/.gitignore
rename to demo-multi-datasource-jpa/.gitignore
diff --git a/demo-multi-datasource-jpa/README.md b/demo-multi-datasource-jpa/README.md
new file mode 100644
index 0000000..326aa93
--- /dev/null
+++ b/demo-multi-datasource-jpa/README.md
@@ -0,0 +1,536 @@
+# spring-boot-demo-multi-datasource-jpa
+
+> 此 demo 主要演示 Spring Boot 如何集成 JPA 的多数据源。
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-multi-datasource-jpa
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-multi-datasource-jpa
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ spring-boot-demo-multi-datasource-jpa
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## PrimaryDataSourceConfig.java
+
+> 主数据源配置
+
+```java
+/**
+ *
+ * JPA多数据源配置 - 主数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 15:58
+ */
+@Configuration
+public class PrimaryDataSourceConfig {
+
+ /**
+ * 扫描spring.datasource.primary开头的配置信息
+ *
+ * @return 数据源配置信息
+ */
+ @Primary
+ @Bean(name = "primaryDataSourceProperties")
+ @ConfigurationProperties(prefix = "spring.datasource.primary")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ /**
+ * 获取主库数据源对象
+ *
+ * @param dataSourceProperties 注入名为primaryDataSourceProperties的bean
+ * @return 数据源对象
+ */
+ @Primary
+ @Bean(name = "primaryDataSource")
+ public DataSource dataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties dataSourceProperties) {
+ return dataSourceProperties.initializeDataSourceBuilder().build();
+ }
+
+ /**
+ * 该方法仅在需要使用JdbcTemplate对象时选用
+ *
+ * @param dataSource 注入名为primaryDataSource的bean
+ * @return 数据源JdbcTemplate对象
+ */
+ @Primary
+ @Bean(name = "primaryJdbcTemplate")
+ public JdbcTemplate jdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+}
+```
+
+## SecondDataSourceConfig.java
+
+> 从数据源配置
+
+```java
+/**
+ *
+ * JPA多数据源配置 - 次数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 15:58
+ */
+@Configuration
+public class SecondDataSourceConfig {
+
+ /**
+ * 扫描spring.datasource.second开头的配置信息
+ *
+ * @return 数据源配置信息
+ */
+ @Bean(name = "secondDataSourceProperties")
+ @ConfigurationProperties(prefix = "spring.datasource.second")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ /**
+ * 获取主库数据源对象
+ *
+ * @param dataSourceProperties 注入名为secondDataSourceProperties的bean
+ * @return 数据源对象
+ */
+ @Bean(name = "secondDataSource")
+ public DataSource dataSource(@Qualifier("secondDataSourceProperties") DataSourceProperties dataSourceProperties) {
+ return dataSourceProperties.initializeDataSourceBuilder().build();
+ }
+
+ /**
+ * 该方法仅在需要使用JdbcTemplate对象时选用
+ *
+ * @param dataSource 注入名为secondDataSource的bean
+ * @return 数据源JdbcTemplate对象
+ */
+ @Bean(name = "secondJdbcTemplate")
+ public JdbcTemplate jdbcTemplate(@Qualifier("secondDataSource") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+}
+```
+
+## PrimaryJpaConfig.java
+
+> 主 JPA 配置
+
+```java
+/**
+ *
+ * JPA多数据源配置 - 主 JPA 配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 16:54
+ */
+@Configuration
+@EnableTransactionManagement
+@EnableJpaRepositories(
+ // repository包名
+ basePackages = PrimaryJpaConfig.REPOSITORY_PACKAGE,
+ // 实体管理bean名称
+ entityManagerFactoryRef = "primaryEntityManagerFactory",
+ // 事务管理bean名称
+ transactionManagerRef = "primaryTransactionManager")
+public class PrimaryJpaConfig {
+ static final String REPOSITORY_PACKAGE = "com.xkcoding.multi.datasource.jpa.repository.primary";
+ private static final String ENTITY_PACKAGE = "com.xkcoding.multi.datasource.jpa.entity.primary";
+
+
+ /**
+ * 扫描spring.jpa.primary开头的配置信息
+ *
+ * @return jpa配置信息
+ */
+ @Primary
+ @Bean(name = "primaryJpaProperties")
+ @ConfigurationProperties(prefix = "spring.jpa.primary")
+ public JpaProperties jpaProperties() {
+ return new JpaProperties();
+ }
+
+ /**
+ * 获取主库实体管理工厂对象
+ *
+ * @param primaryDataSource 注入名为primaryDataSource的数据源
+ * @param jpaProperties 注入名为primaryJpaProperties的jpa配置信息
+ * @param builder 注入EntityManagerFactoryBuilder
+ * @return 实体管理工厂对象
+ */
+ @Primary
+ @Bean(name = "primaryEntityManagerFactory")
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("primaryDataSource") DataSource primaryDataSource, @Qualifier("primaryJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder) {
+ return builder
+ // 设置数据源
+ .dataSource(primaryDataSource)
+ // 设置jpa配置
+ .properties(jpaProperties.getProperties())
+ // 设置实体包名
+ .packages(ENTITY_PACKAGE)
+ // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
+ .persistenceUnit("primaryPersistenceUnit").build();
+ }
+
+ /**
+ * 获取实体管理对象
+ *
+ * @param factory 注入名为primaryEntityManagerFactory的bean
+ * @return 实体管理对象
+ */
+ @Primary
+ @Bean(name = "primaryEntityManager")
+ public EntityManager entityManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
+ return factory.createEntityManager();
+ }
+
+ /**
+ * 获取主库事务管理对象
+ *
+ * @param factory 注入名为primaryEntityManagerFactory的bean
+ * @return 事务管理对象
+ */
+ @Primary
+ @Bean(name = "primaryTransactionManager")
+ public PlatformTransactionManager transactionManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
+ return new JpaTransactionManager(factory);
+ }
+
+}
+```
+
+## SecondJpaConfig.java
+
+> 从 JPA 配置
+
+```java
+/**
+ *
+ * JPA多数据源配置 - 次 JPA 配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 16:54
+ */
+@Configuration
+@EnableTransactionManagement
+@EnableJpaRepositories(
+ // repository包名
+ basePackages = SecondJpaConfig.REPOSITORY_PACKAGE,
+ // 实体管理bean名称
+ entityManagerFactoryRef = "secondEntityManagerFactory",
+ // 事务管理bean名称
+ transactionManagerRef = "secondTransactionManager")
+public class SecondJpaConfig {
+ static final String REPOSITORY_PACKAGE = "com.xkcoding.multi.datasource.jpa.repository.second";
+ private static final String ENTITY_PACKAGE = "com.xkcoding.multi.datasource.jpa.entity.second";
+
+
+ /**
+ * 扫描spring.jpa.second开头的配置信息
+ *
+ * @return jpa配置信息
+ */
+ @Bean(name = "secondJpaProperties")
+ @ConfigurationProperties(prefix = "spring.jpa.second")
+ public JpaProperties jpaProperties() {
+ return new JpaProperties();
+ }
+
+ /**
+ * 获取主库实体管理工厂对象
+ *
+ * @param secondDataSource 注入名为secondDataSource的数据源
+ * @param jpaProperties 注入名为secondJpaProperties的jpa配置信息
+ * @param builder 注入EntityManagerFactoryBuilder
+ * @return 实体管理工厂对象
+ */
+ @Bean(name = "secondEntityManagerFactory")
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("secondDataSource") DataSource secondDataSource, @Qualifier("secondJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder) {
+ return builder
+ // 设置数据源
+ .dataSource(secondDataSource)
+ // 设置jpa配置
+ .properties(jpaProperties.getProperties())
+ // 设置实体包名
+ .packages(ENTITY_PACKAGE)
+ // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
+ .persistenceUnit("secondPersistenceUnit").build();
+ }
+
+ /**
+ * 获取实体管理对象
+ *
+ * @param factory 注入名为secondEntityManagerFactory的bean
+ * @return 实体管理对象
+ */
+ @Bean(name = "secondEntityManager")
+ public EntityManager entityManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory factory) {
+ return factory.createEntityManager();
+ }
+
+ /**
+ * 获取主库事务管理对象
+ *
+ * @param factory 注入名为secondEntityManagerFactory的bean
+ * @return 事务管理对象
+ */
+ @Bean(name = "secondTransactionManager")
+ public PlatformTransactionManager transactionManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory factory) {
+ return new JpaTransactionManager(factory);
+ }
+
+}
+```
+
+## application.yml
+
+```yaml
+spring:
+ datasource:
+ primary:
+ url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
+ username: root
+ password: root
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ type: com.zaxxer.hikari.HikariDataSource
+ hikari:
+ minimum-idle: 5
+ connection-test-query: SELECT 1 FROM DUAL
+ maximum-pool-size: 20
+ auto-commit: true
+ idle-timeout: 30000
+ pool-name: PrimaryHikariCP
+ max-lifetime: 60000
+ connection-timeout: 30000
+ second:
+ url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
+ username: root
+ password: root
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ type: com.zaxxer.hikari.HikariDataSource
+ hikari:
+ minimum-idle: 5
+ connection-test-query: SELECT 1 FROM DUAL
+ maximum-pool-size: 20
+ auto-commit: true
+ idle-timeout: 30000
+ pool-name: SecondHikariCP
+ max-lifetime: 60000
+ connection-timeout: 30000
+ jpa:
+ primary:
+ show-sql: true
+ generate-ddl: true
+ hibernate:
+ ddl-auto: update
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.MySQL57InnoDBDialect
+ open-in-view: true
+ second:
+ show-sql: true
+ generate-ddl: true
+ hibernate:
+ ddl-auto: update
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.MySQL57InnoDBDialect
+ open-in-view: true
+logging:
+ level:
+ com.xkcoding: debug
+ org.hibernate.SQL: debug
+ org.hibernate.type: trace
+```
+
+## SpringBootDemoMultiDatasourceJpaApplicationTests.java
+
+```java
+package com.xkcoding.multi.datasource.jpa;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.lang.Snowflake;
+import com.xkcoding.multi.datasource.jpa.entity.primary.PrimaryMultiTable;
+import com.xkcoding.multi.datasource.jpa.entity.second.SecondMultiTable;
+import com.xkcoding.multi.datasource.jpa.repository.primary.PrimaryMultiTableRepository;
+import com.xkcoding.multi.datasource.jpa.repository.second.SecondMultiTableRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@Slf4j
+public class SpringBootDemoMultiDatasourceJpaApplicationTests {
+ @Autowired
+ private PrimaryMultiTableRepository primaryRepo;
+ @Autowired
+ private SecondMultiTableRepository secondRepo;
+ @Autowired
+ private Snowflake snowflake;
+
+ @Test
+ public void testInsert() {
+ PrimaryMultiTable primary = new PrimaryMultiTable(snowflake.nextId(),"测试名称-1");
+ primaryRepo.save(primary);
+
+ SecondMultiTable second = new SecondMultiTable();
+ BeanUtil.copyProperties(primary, second);
+ secondRepo.save(second);
+ }
+
+ @Test
+ public void testUpdate() {
+ primaryRepo.findAll().forEach(primary -> {
+ primary.setName("修改后的"+primary.getName());
+ primaryRepo.save(primary);
+
+ SecondMultiTable second = new SecondMultiTable();
+ BeanUtil.copyProperties(primary, second);
+ secondRepo.save(second);
+ });
+ }
+
+ @Test
+ public void testDelete() {
+ primaryRepo.deleteAll();
+
+ secondRepo.deleteAll();
+ }
+
+ @Test
+ public void testSelect() {
+ List primary = primaryRepo.findAll();
+ log.info("【primary】= {}", primary);
+
+ List second = secondRepo.findAll();
+ log.info("【second】= {}", second);
+ }
+
+}
+```
+
+## 目录结构
+
+```
+.
+├── README.md
+├── pom.xml
+├── spring-boot-demo-multi-datasource-jpa.iml
+├── src
+│ ├── main
+│ │ ├── java
+│ │ │ └── com.xkcoding.multi.datasource.jpa
+│ │ │ ├── SpringBootDemoMultiDatasourceJpaApplication.java
+│ │ │ ├── config
+│ │ │ │ ├── PrimaryDataSourceConfig.java
+│ │ │ │ ├── PrimaryJpaConfig.java
+│ │ │ │ ├── SecondDataSourceConfig.java
+│ │ │ │ ├── SecondJpaConfig.java
+│ │ │ │ └── SnowflakeConfig.java
+│ │ │ ├── entity
+│ │ │ │ ├── primary
+│ │ │ │ │ └── PrimaryMultiTable.java
+│ │ │ │ └── second
+│ │ │ │ └── SecondMultiTable.java
+│ │ │ └── repository
+│ │ │ ├── primary
+│ │ │ │ └── PrimaryMultiTableRepository.java
+│ │ │ └── second
+│ │ │ └── SecondMultiTableRepository.java
+│ │ └── resources
+│ │ └── application.yml
+│ └── test
+│ └── java
+│ └── com.xkcoding.multi.datasource.jpa
+│ └── SpringBootDemoMultiDatasourceJpaApplicationTests.java
+└── target
+```
+
+## 参考
+
+1. https://www.jianshu.com/p/34730e595a8c
+2. https://blog.csdn.net/anxpp/article/details/52274120
diff --git a/demo-multi-datasource-jpa/pom.xml b/demo-multi-datasource-jpa/pom.xml
new file mode 100644
index 0000000..a14af77
--- /dev/null
+++ b/demo-multi-datasource-jpa/pom.xml
@@ -0,0 +1,74 @@
+
+
+ 4.0.0
+
+ demo-multi-datasource-jpa
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-multi-datasource-jpa
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ demo-multi-datasource-jpa
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplication.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplication.java
new file mode 100644
index 0000000..e5451c2
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.multi.datasource.jpa;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-16 17:34
+ */
+@SpringBootApplication
+public class SpringBootDemoMultiDatasourceJpaApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoMultiDatasourceJpaApplication.class, args);
+ }
+
+}
+
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryDataSourceConfig.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryDataSourceConfig.java
new file mode 100644
index 0000000..fcfcb25
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryDataSourceConfig.java
@@ -0,0 +1,60 @@
+package com.xkcoding.multi.datasource.jpa.config;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+
+/**
+ *
+ * JPA多数据源配置 - 主数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 15:58
+ */
+@Configuration
+public class PrimaryDataSourceConfig {
+
+ /**
+ * 扫描spring.datasource.primary开头的配置信息
+ *
+ * @return 数据源配置信息
+ */
+ @Primary
+ @Bean(name = "primaryDataSourceProperties")
+ @ConfigurationProperties(prefix = "spring.datasource.primary")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ /**
+ * 获取主库数据源对象
+ *
+ * @param dataSourceProperties 注入名为primaryDataSourceProperties的bean
+ * @return 数据源对象
+ */
+ @Primary
+ @Bean(name = "primaryDataSource")
+ public DataSource dataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties dataSourceProperties) {
+ return dataSourceProperties.initializeDataSourceBuilder().build();
+ }
+
+ /**
+ * 该方法仅在需要使用JdbcTemplate对象时选用
+ *
+ * @param dataSource 注入名为primaryDataSource的bean
+ * @return 数据源JdbcTemplate对象
+ */
+ @Primary
+ @Bean(name = "primaryJdbcTemplate")
+ public JdbcTemplate jdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryJpaConfig.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryJpaConfig.java
new file mode 100644
index 0000000..39f93e6
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryJpaConfig.java
@@ -0,0 +1,100 @@
+package com.xkcoding.multi.datasource.jpa.config;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+/**
+ *
+ * JPA多数据源配置 - 主 JPA 配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 16:54
+ */
+@Configuration
+@EnableTransactionManagement
+@EnableJpaRepositories(
+ // repository包名
+ basePackages = PrimaryJpaConfig.REPOSITORY_PACKAGE,
+ // 实体管理bean名称
+ entityManagerFactoryRef = "primaryEntityManagerFactory",
+ // 事务管理bean名称
+ transactionManagerRef = "primaryTransactionManager")
+public class PrimaryJpaConfig {
+ static final String REPOSITORY_PACKAGE = "com.xkcoding.multi.datasource.jpa.repository.primary";
+ private static final String ENTITY_PACKAGE = "com.xkcoding.multi.datasource.jpa.entity.primary";
+
+
+ /**
+ * 扫描spring.jpa.primary开头的配置信息
+ *
+ * @return jpa配置信息
+ */
+ @Primary
+ @Bean(name = "primaryJpaProperties")
+ @ConfigurationProperties(prefix = "spring.jpa.primary")
+ public JpaProperties jpaProperties() {
+ return new JpaProperties();
+ }
+
+ /**
+ * 获取主库实体管理工厂对象
+ *
+ * @param primaryDataSource 注入名为primaryDataSource的数据源
+ * @param jpaProperties 注入名为primaryJpaProperties的jpa配置信息
+ * @param builder 注入EntityManagerFactoryBuilder
+ * @return 实体管理工厂对象
+ */
+ @Primary
+ @Bean(name = "primaryEntityManagerFactory")
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("primaryDataSource") DataSource primaryDataSource, @Qualifier("primaryJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder) {
+ return builder
+ // 设置数据源
+ .dataSource(primaryDataSource)
+ // 设置jpa配置
+ .properties(jpaProperties.getProperties())
+ // 设置实体包名
+ .packages(ENTITY_PACKAGE)
+ // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
+ .persistenceUnit("primaryPersistenceUnit").build();
+ }
+
+ /**
+ * 获取实体管理对象
+ *
+ * @param factory 注入名为primaryEntityManagerFactory的bean
+ * @return 实体管理对象
+ */
+ @Primary
+ @Bean(name = "primaryEntityManager")
+ public EntityManager entityManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
+ return factory.createEntityManager();
+ }
+
+ /**
+ * 获取主库事务管理对象
+ *
+ * @param factory 注入名为primaryEntityManagerFactory的bean
+ * @return 事务管理对象
+ */
+ @Primary
+ @Bean(name = "primaryTransactionManager")
+ public PlatformTransactionManager transactionManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory factory) {
+ return new JpaTransactionManager(factory);
+ }
+
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondDataSourceConfig.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondDataSourceConfig.java
new file mode 100644
index 0000000..49b7746
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondDataSourceConfig.java
@@ -0,0 +1,56 @@
+package com.xkcoding.multi.datasource.jpa.config;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+
+/**
+ *
+ * JPA多数据源配置 - 次数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 15:58
+ */
+@Configuration
+public class SecondDataSourceConfig {
+
+ /**
+ * 扫描spring.datasource.second开头的配置信息
+ *
+ * @return 数据源配置信息
+ */
+ @Bean(name = "secondDataSourceProperties")
+ @ConfigurationProperties(prefix = "spring.datasource.second")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ /**
+ * 获取主库数据源对象
+ *
+ * @param dataSourceProperties 注入名为secondDataSourceProperties的bean
+ * @return 数据源对象
+ */
+ @Bean(name = "secondDataSource")
+ public DataSource dataSource(@Qualifier("secondDataSourceProperties") DataSourceProperties dataSourceProperties) {
+ return dataSourceProperties.initializeDataSourceBuilder().build();
+ }
+
+ /**
+ * 该方法仅在需要使用JdbcTemplate对象时选用
+ *
+ * @param dataSource 注入名为secondDataSource的bean
+ * @return 数据源JdbcTemplate对象
+ */
+ @Bean(name = "secondJdbcTemplate")
+ public JdbcTemplate jdbcTemplate(@Qualifier("secondDataSource") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondJpaConfig.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondJpaConfig.java
new file mode 100644
index 0000000..ebbc349
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondJpaConfig.java
@@ -0,0 +1,95 @@
+package com.xkcoding.multi.datasource.jpa.config;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+/**
+ *
+ * JPA多数据源配置 - 次 JPA 配置
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-17 16:54
+ */
+@Configuration
+@EnableTransactionManagement
+@EnableJpaRepositories(
+ // repository包名
+ basePackages = SecondJpaConfig.REPOSITORY_PACKAGE,
+ // 实体管理bean名称
+ entityManagerFactoryRef = "secondEntityManagerFactory",
+ // 事务管理bean名称
+ transactionManagerRef = "secondTransactionManager")
+public class SecondJpaConfig {
+ static final String REPOSITORY_PACKAGE = "com.xkcoding.multi.datasource.jpa.repository.second";
+ private static final String ENTITY_PACKAGE = "com.xkcoding.multi.datasource.jpa.entity.second";
+
+
+ /**
+ * 扫描spring.jpa.second开头的配置信息
+ *
+ * @return jpa配置信息
+ */
+ @Bean(name = "secondJpaProperties")
+ @ConfigurationProperties(prefix = "spring.jpa.second")
+ public JpaProperties jpaProperties() {
+ return new JpaProperties();
+ }
+
+ /**
+ * 获取主库实体管理工厂对象
+ *
+ * @param secondDataSource 注入名为secondDataSource的数据源
+ * @param jpaProperties 注入名为secondJpaProperties的jpa配置信息
+ * @param builder 注入EntityManagerFactoryBuilder
+ * @return 实体管理工厂对象
+ */
+ @Bean(name = "secondEntityManagerFactory")
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("secondDataSource") DataSource secondDataSource, @Qualifier("secondJpaProperties") JpaProperties jpaProperties, EntityManagerFactoryBuilder builder) {
+ return builder
+ // 设置数据源
+ .dataSource(secondDataSource)
+ // 设置jpa配置
+ .properties(jpaProperties.getProperties())
+ // 设置实体包名
+ .packages(ENTITY_PACKAGE)
+ // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
+ .persistenceUnit("secondPersistenceUnit").build();
+ }
+
+ /**
+ * 获取实体管理对象
+ *
+ * @param factory 注入名为secondEntityManagerFactory的bean
+ * @return 实体管理对象
+ */
+ @Bean(name = "secondEntityManager")
+ public EntityManager entityManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory factory) {
+ return factory.createEntityManager();
+ }
+
+ /**
+ * 获取主库事务管理对象
+ *
+ * @param factory 注入名为secondEntityManagerFactory的bean
+ * @return 事务管理对象
+ */
+ @Bean(name = "secondTransactionManager")
+ public PlatformTransactionManager transactionManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory factory) {
+ return new JpaTransactionManager(factory);
+ }
+
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SnowflakeConfig.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SnowflakeConfig.java
new file mode 100644
index 0000000..2508fb9
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SnowflakeConfig.java
@@ -0,0 +1,22 @@
+package com.xkcoding.multi.datasource.jpa.config;
+
+import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.util.IdUtil;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ *
+ * 雪花算法生成器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-18 15:50
+ */
+@Configuration
+public class SnowflakeConfig {
+ @Bean
+ public Snowflake snowflake() {
+ return IdUtil.createSnowflake(1, 1);
+ }
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/primary/PrimaryMultiTable.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/primary/PrimaryMultiTable.java
new file mode 100644
index 0000000..4904e69
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/primary/PrimaryMultiTable.java
@@ -0,0 +1,37 @@
+package com.xkcoding.multi.datasource.jpa.entity.primary;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ *
+ * 多数据源测试表
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-18 10:06
+ */
+@Data
+@Entity
+@Table(name = "multi_table")
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class PrimaryMultiTable {
+ /**
+ * 主键
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 名称
+ */
+ private String name;
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/second/SecondMultiTable.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/second/SecondMultiTable.java
new file mode 100644
index 0000000..3756681
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/second/SecondMultiTable.java
@@ -0,0 +1,37 @@
+package com.xkcoding.multi.datasource.jpa.entity.second;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ *
+ * 多数据源测试表
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-18 10:06
+ */
+@Data
+@Entity
+@Table(name = "multi_table")
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SecondMultiTable {
+ /**
+ * 主键
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 名称
+ */
+ private String name;
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/primary/PrimaryMultiTableRepository.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/primary/PrimaryMultiTableRepository.java
new file mode 100644
index 0000000..91cd78b
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/primary/PrimaryMultiTableRepository.java
@@ -0,0 +1,17 @@
+package com.xkcoding.multi.datasource.jpa.repository.primary;
+
+import com.xkcoding.multi.datasource.jpa.entity.primary.PrimaryMultiTable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ *
+ * 多数据源测试 repo
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-18 10:11
+ */
+@Repository
+public interface PrimaryMultiTableRepository extends JpaRepository {
+}
diff --git a/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/second/SecondMultiTableRepository.java b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/second/SecondMultiTableRepository.java
new file mode 100644
index 0000000..0fc6ba8
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/second/SecondMultiTableRepository.java
@@ -0,0 +1,17 @@
+package com.xkcoding.multi.datasource.jpa.repository.second;
+
+import com.xkcoding.multi.datasource.jpa.entity.second.SecondMultiTable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ *
+ * 多数据源测试 repo
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-18 10:11
+ */
+@Repository
+public interface SecondMultiTableRepository extends JpaRepository {
+}
diff --git a/spring-boot-demo-multi-datasource-jpa/src/main/resources/application.yml b/demo-multi-datasource-jpa/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-multi-datasource-jpa/src/main/resources/application.yml
rename to demo-multi-datasource-jpa/src/main/resources/application.yml
diff --git a/demo-multi-datasource-jpa/src/test/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplicationTests.java b/demo-multi-datasource-jpa/src/test/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplicationTests.java
new file mode 100644
index 0000000..9e6bc4d
--- /dev/null
+++ b/demo-multi-datasource-jpa/src/test/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplicationTests.java
@@ -0,0 +1,68 @@
+package com.xkcoding.multi.datasource.jpa;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.lang.Snowflake;
+import com.xkcoding.multi.datasource.jpa.entity.primary.PrimaryMultiTable;
+import com.xkcoding.multi.datasource.jpa.entity.second.SecondMultiTable;
+import com.xkcoding.multi.datasource.jpa.repository.primary.PrimaryMultiTableRepository;
+import com.xkcoding.multi.datasource.jpa.repository.second.SecondMultiTableRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@Slf4j
+public class SpringBootDemoMultiDatasourceJpaApplicationTests {
+ @Autowired
+ private PrimaryMultiTableRepository primaryRepo;
+ @Autowired
+ private SecondMultiTableRepository secondRepo;
+ @Autowired
+ private Snowflake snowflake;
+
+ @Test
+ public void testInsert() {
+ PrimaryMultiTable primary = new PrimaryMultiTable(snowflake.nextId(), "测试名称-1");
+ primaryRepo.save(primary);
+
+ SecondMultiTable second = new SecondMultiTable();
+ BeanUtil.copyProperties(primary, second);
+ secondRepo.save(second);
+ }
+
+ @Test
+ public void testUpdate() {
+ primaryRepo.findAll().forEach(primary -> {
+ primary.setName("修改后的" + primary.getName());
+ primaryRepo.save(primary);
+
+ SecondMultiTable second = new SecondMultiTable();
+ BeanUtil.copyProperties(primary, second);
+ secondRepo.save(second);
+ });
+ }
+
+ @Test
+ public void testDelete() {
+ primaryRepo.deleteAll();
+
+ secondRepo.deleteAll();
+ }
+
+ @Test
+ public void testSelect() {
+ List primary = primaryRepo.findAll();
+ log.info("【primary】= {}", primary);
+
+ List second = secondRepo.findAll();
+ log.info("【second】= {}", second);
+ }
+
+}
+
diff --git a/spring-boot-demo-multi-datasource-mybatis/.gitignore b/demo-multi-datasource-mybatis/.gitignore
similarity index 100%
rename from spring-boot-demo-multi-datasource-mybatis/.gitignore
rename to demo-multi-datasource-mybatis/.gitignore
diff --git a/demo-multi-datasource-mybatis/README.md b/demo-multi-datasource-mybatis/README.md
new file mode 100644
index 0000000..0bacd48
--- /dev/null
+++ b/demo-multi-datasource-mybatis/README.md
@@ -0,0 +1,352 @@
+# spring-boot-demo-multi-datasource-mybatis
+
+> 此 demo 主要演示了 Spring Boot 如何集成 Mybatis 的多数据源。可以自己基于AOP实现多数据源,这里基于 Mybatis-Plus 提供的一个优雅的开源的解决方案来实现。
+
+## 准备工作
+
+准备两个数据源,分别执行如下建表语句
+
+```mysql
+DROP TABLE IF EXISTS `multi_user`;
+CREATE TABLE `multi_user`(
+ `id` bigint(64) NOT NULL,
+ `name` varchar(50) DEFAULT NULL,
+ `age` int(30) DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB
+ AUTO_INCREMENT = 1
+ CHARACTER SET = utf8
+ COLLATE = utf8_general_ci;
+```
+
+## 导入依赖
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-multi-datasource-mybatis
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-multi-datasource-mybatis
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot-starter
+ 2.5.0
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ 3.0.7.1
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ spring-boot-demo-multi-datasource-mybatis
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## 准备实体类
+
+`User.java`
+
+> 1. @Data / @NoArgsConstructor / @AllArgsConstructor / @Builder 都是 lombok 注解
+> 2. @TableName("multi_user") 是 Mybatis-Plus 注解,主要是当实体类名字和表名不满足 **驼峰和下划线互转** 的格式时,用于表示数据库表名
+> 3. @TableId(type = IdType.ID_WORKER) 是 Mybatis-Plus 注解,主要是指定主键类型,这里我使用的是 Mybatis-Plus 基于 twitter 提供的 雪花算法
+
+```java
+/**
+ *
+ * User实体类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:19
+ */
+@Data
+@TableName("multi_user")
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class User implements Serializable {
+ private static final long serialVersionUID = -1923859222295750467L;
+
+ /**
+ * 主键
+ */
+ @TableId(type = IdType.ID_WORKER)
+ private Long id;
+
+ /**
+ * 姓名
+ */
+ private String name;
+
+ /**
+ * 年龄
+ */
+ private Integer age;
+}
+```
+
+## 数据访问层
+
+`UserMapper.java`
+
+> 不需要建对应的xml,只需要继承 BaseMapper 就拥有了大部分单表操作的方法了。
+
+```java
+/**
+ *
+ * 数据访问层
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:28
+ */
+public interface UserMapper extends BaseMapper {
+}
+```
+
+## 数据服务层
+
+### 接口
+
+`UserService.java`
+
+```java
+/**
+ *
+ * 数据服务层
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:31
+ */
+public interface UserService extends IService {
+
+ /**
+ * 添加 User
+ *
+ * @param user 用户
+ */
+ void addUser(User user);
+}
+```
+
+### 实现
+
+`UserServiceImpl.java`
+
+> 1. @DS: 注解在类上或方法上来切换数据源,方法上的@DS优先级大于类上的@DS
+> 2. baseMapper: mapper 对象,即`UserMapper`,可获得CRUD功能
+> 3. 默认走从库: `@DS(value = "slave")`在类上,默认走从库,除非在方法在添加`@DS(value = "master")`才走主库
+
+```java
+/**
+ *
+ * 数据服务层 实现
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:37
+ */
+@Service
+@DS("slave")
+public class UserServiceImpl extends ServiceImpl implements UserService {
+
+ /**
+ * 类上 {@code @DS("slave")} 代表默认从库,在方法上写 {@code @DS("master")} 代表默认主库
+ *
+ * @param user 用户
+ */
+ @DS("master")
+ @Override
+ public void addUser(User user) {
+ baseMapper.insert(user);
+ }
+}
+```
+
+## 启动类
+
+`SpringBootDemoMultiDatasourceMybatisApplication.java`
+
+> 启动类上方需要使用@MapperScan扫描 mapper 类所在的包
+
+```java
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:19
+ */
+@SpringBootApplication
+@MapperScan(basePackages = "com.xkcoding.multi.datasource.mybatis.mapper")
+public class SpringBootDemoMultiDatasourceMybatisApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoMultiDatasourceMybatisApplication.class, args);
+ }
+
+}
+```
+
+## 配置文件
+
+`application.yml`
+
+```yaml
+spring:
+ datasource:
+ dynamic:
+ datasource:
+ master:
+ username: root
+ password: root
+ url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ slave:
+ username: root
+ password: root
+ url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ mp-enabled: true
+logging:
+ level:
+ com.xkcoding.multi.datasource.mybatis: debug
+```
+
+## 测试类
+
+```java
+/**
+ *
+ * 测试主从数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:45
+ */
+@Slf4j
+public class UserServiceImplTest extends SpringBootDemoMultiDatasourceMybatisApplicationTests {
+ @Autowired
+ private UserService userService;
+
+ /**
+ * 主从库添加
+ */
+ @Test
+ public void addUser() {
+ User userMaster = User.builder().name("主库添加").age(20).build();
+ userService.addUser(userMaster);
+
+ User userSlave = User.builder().name("从库添加").age(20).build();
+ userService.save(userSlave);
+ }
+
+ /**
+ * 从库查询
+ */
+ @Test
+ public void testListUser() {
+ List list = userService.list(new QueryWrapper<>());
+ log.info("【list】= {}", JSONUtil.toJsonStr(list));
+ }
+}
+```
+
+### 测试结果
+
+主从数据源加载成功
+
+```java
+2019-01-21 14:55:41.096 INFO 7239 --- [ main] com.zaxxer.hikari.HikariDataSource : master - Starting...
+2019-01-21 14:55:41.307 INFO 7239 --- [ main] com.zaxxer.hikari.HikariDataSource : master - Start completed.
+2019-01-21 14:55:41.308 INFO 7239 --- [ main] com.zaxxer.hikari.HikariDataSource : slave - Starting...
+2019-01-21 14:55:41.312 INFO 7239 --- [ main] com.zaxxer.hikari.HikariDataSource : slave - Start completed.
+2019-01-21 14:55:41.312 INFO 7239 --- [ main] c.b.d.d.DynamicRoutingDataSource : 初始共加载 2 个数据源
+2019-01-21 14:55:41.313 INFO 7239 --- [ main] c.b.d.d.DynamicRoutingDataSource : 动态数据源-加载 slave 成功
+2019-01-21 14:55:41.313 INFO 7239 --- [ main] c.b.d.d.DynamicRoutingDataSource : 动态数据源-加载 master 成功
+2019-01-21 14:55:41.313 INFO 7239 --- [ main] c.b.d.d.DynamicRoutingDataSource : 当前的默认数据源是单数据源,数据源名为 master
+ _ _ |_ _ _|_. ___ _ | _
+| | |\/|_)(_| | |_\ |_)||_|_\
+ / |
+ 3.0.7.1
+```
+
+**主**库 **建议** 只执行 **INSERT** **UPDATE** **DELETE** 操作
+
+![image-20190121153211509](http://static.xkcoding.com/spring-boot-demo/multi-datasource/mybatis/063506.jpg)
+
+**从**库 **建议** 只执行 **SELECT** 操作
+
+![image-20190121152825859](http://static.xkcoding.com/spring-boot-demo/multi-datasource/mybatis/063505.jpg)
+
+> 生产环境需要搭建 **主从复制**
+
+## 参考
+
+1. Mybatis-Plus 多数据源文档:https://mybatis.plus/guide/dynamic-datasource.html
+2. Mybatis-Plus 多数据源集成官方 demo:https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter/tree/master/samples
diff --git a/demo-multi-datasource-mybatis/pom.xml b/demo-multi-datasource-mybatis/pom.xml
new file mode 100644
index 0000000..f4502ba
--- /dev/null
+++ b/demo-multi-datasource-mybatis/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+ demo-multi-datasource-mybatis
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-multi-datasource-mybatis
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot-starter
+ 2.5.0
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ 3.1.0
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ demo-multi-datasource-mybatis
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/spring-boot-demo-multi-datasource-mybatis/sql/db.sql b/demo-multi-datasource-mybatis/sql/db.sql
similarity index 100%
rename from spring-boot-demo-multi-datasource-mybatis/sql/db.sql
rename to demo-multi-datasource-mybatis/sql/db.sql
diff --git a/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplication.java b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplication.java
new file mode 100644
index 0000000..bdeb42b
--- /dev/null
+++ b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplication.java
@@ -0,0 +1,24 @@
+package com.xkcoding.multi.datasource.mybatis;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:19
+ */
+@SpringBootApplication
+@MapperScan(basePackages = "com.xkcoding.multi.datasource.mybatis.mapper")
+public class SpringBootDemoMultiDatasourceMybatisApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoMultiDatasourceMybatisApplication.class, args);
+ }
+
+}
+
diff --git a/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/mapper/UserMapper.java b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/mapper/UserMapper.java
new file mode 100644
index 0000000..3e8999c
--- /dev/null
+++ b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/mapper/UserMapper.java
@@ -0,0 +1,15 @@
+package com.xkcoding.multi.datasource.mybatis.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.xkcoding.multi.datasource.mybatis.model.User;
+
+/**
+ *
+ * 数据访问层
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:28
+ */
+public interface UserMapper extends BaseMapper {
+}
diff --git a/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/model/User.java b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/model/User.java
new file mode 100644
index 0000000..6790049
--- /dev/null
+++ b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/model/User.java
@@ -0,0 +1,44 @@
+package com.xkcoding.multi.datasource.mybatis.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * User实体类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:19
+ */
+@Data
+@TableName("multi_user")
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class User implements Serializable {
+ private static final long serialVersionUID = -1923859222295750467L;
+
+ /**
+ * 主键
+ */
+ @TableId(type = IdType.ID_WORKER)
+ private Long id;
+
+ /**
+ * 姓名
+ */
+ private String name;
+
+ /**
+ * 年龄
+ */
+ private Integer age;
+}
diff --git a/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/UserService.java b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/UserService.java
new file mode 100644
index 0000000..8e80563
--- /dev/null
+++ b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/UserService.java
@@ -0,0 +1,22 @@
+package com.xkcoding.multi.datasource.mybatis.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.xkcoding.multi.datasource.mybatis.model.User;
+
+/**
+ *
+ * 数据服务层
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:31
+ */
+public interface UserService extends IService {
+
+ /**
+ * 添加 User
+ *
+ * @param user 用户
+ */
+ void addUser(User user);
+}
diff --git a/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImpl.java b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..65a4f5a
--- /dev/null
+++ b/demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImpl.java
@@ -0,0 +1,32 @@
+package com.xkcoding.multi.datasource.mybatis.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.xkcoding.multi.datasource.mybatis.mapper.UserMapper;
+import com.xkcoding.multi.datasource.mybatis.model.User;
+import com.xkcoding.multi.datasource.mybatis.service.UserService;
+import org.springframework.stereotype.Service;
+
+/**
+ *
+ * 数据服务层 实现
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:37
+ */
+@Service
+@DS("slave")
+public class UserServiceImpl extends ServiceImpl implements UserService {
+
+ /**
+ * 类上 {@code @DS("slave")} 代表默认从库,在方法上写 {@code @DS("master")} 代表默认主库
+ *
+ * @param user 用户
+ */
+ @DS("master")
+ @Override
+ public void addUser(User user) {
+ baseMapper.insert(user);
+ }
+}
diff --git a/spring-boot-demo-multi-datasource-mybatis/src/main/resources/application.yml b/demo-multi-datasource-mybatis/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-multi-datasource-mybatis/src/main/resources/application.yml
rename to demo-multi-datasource-mybatis/src/main/resources/application.yml
diff --git a/spring-boot-demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplicationTests.java b/demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplicationTests.java
similarity index 100%
rename from spring-boot-demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplicationTests.java
rename to demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplicationTests.java
diff --git a/demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImplTest.java b/demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImplTest.java
new file mode 100644
index 0000000..fe11b6d
--- /dev/null
+++ b/demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImplTest.java
@@ -0,0 +1,47 @@
+package com.xkcoding.multi.datasource.mybatis.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.xkcoding.multi.datasource.mybatis.SpringBootDemoMultiDatasourceMybatisApplicationTests;
+import com.xkcoding.multi.datasource.mybatis.model.User;
+import com.xkcoding.multi.datasource.mybatis.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+
+/**
+ *
+ * 测试主从数据源
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-01-21 14:45
+ */
+@Slf4j
+public class UserServiceImplTest extends SpringBootDemoMultiDatasourceMybatisApplicationTests {
+ @Autowired
+ private UserService userService;
+
+ /**
+ * 主从库添加
+ */
+ @Test
+ public void addUser() {
+ User userMaster = User.builder().name("主库添加").age(20).build();
+ userService.addUser(userMaster);
+
+ User userSlave = User.builder().name("从库添加").age(20).build();
+ userService.save(userSlave);
+ }
+
+ /**
+ * 从库查询
+ */
+ @Test
+ public void testListUser() {
+ List list = userService.list(new QueryWrapper<>());
+ log.info("【list】= {}", JSONUtil.toJsonStr(list));
+ }
+}
diff --git a/spring-boot-demo-neo4j/.gitignore b/demo-neo4j/.gitignore
similarity index 100%
rename from spring-boot-demo-neo4j/.gitignore
rename to demo-neo4j/.gitignore
diff --git a/demo-neo4j/README.md b/demo-neo4j/README.md
new file mode 100644
index 0000000..0321a99
--- /dev/null
+++ b/demo-neo4j/README.md
@@ -0,0 +1,319 @@
+# spring-boot-demo-neo4j
+
+> 此 demo 主要演示了 Spring Boot 如何集成Neo4j操作图数据库,实现一个校园人物关系网。
+
+## 注意
+
+作者编写本demo时,Neo4j 版本为 `3.5.0`,使用 docker 运行,下面是所有步骤:
+
+1. 下载镜像:`docker pull neo4j:3.5.0`
+2. 运行容器:`docker run -d -p 7474:7474 -p 7687:7687 --name neo4j-3.5.0 neo4j:3.5.0`
+3. 停止容器:`docker stop neo4j-3.5.0`
+4. 启动容器:`docker start neo4j-3.5.0`
+5. 浏览器 http://localhost:7474/ 访问 neo4j 管理后台,初始账号/密码 neo4j/neo4j,会要求修改初始化密码,我们修改为 neo4j/admin
+
+## pom.xml
+
+```xml
+
+
+ 4.0.0
+
+ spring-boot-demo-neo4j
+ 1.0.0-SNAPSHOT
+ jar
+
+ spring-boot-demo-neo4j
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-neo4j
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ spring-boot-demo-neo4j
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+```
+
+## application.yml
+
+```yaml
+spring:
+ data:
+ neo4j:
+ uri: bolt://localhost
+ username: neo4j
+ password: admin
+ open-in-view: false
+```
+
+## CustomIdStrategy.java
+
+```java
+/**
+ *
+ * 自定义主键策略
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:40
+ */
+public class CustomIdStrategy implements IdStrategy {
+ @Override
+ public Object generateId(Object o) {
+ return IdUtil.fastUUID();
+ }
+}
+```
+
+## 部分Model代码
+
+### Student.java
+
+```java
+/**
+ *
+ * 学生节点
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:38
+ */
+@Data
+@NoArgsConstructor
+@RequiredArgsConstructor(staticName = "of")
+@AllArgsConstructor
+@Builder
+@NodeEntity
+public class Student {
+ /**
+ * 主键,自定义主键策略,使用UUID生成
+ */
+ @Id
+ @GeneratedValue(strategy = CustomIdStrategy.class)
+ private String id;
+
+ /**
+ * 学生姓名
+ */
+ @NonNull
+ private String name;
+
+ /**
+ * 学生选的所有课程
+ */
+ @Relationship(NeoConsts.R_LESSON_OF_STUDENT)
+ @NonNull
+ private List lessons;
+
+ /**
+ * 学生所在班级
+ */
+ @Relationship(NeoConsts.R_STUDENT_OF_CLASS)
+ @NonNull
+ private Class clazz;
+
+}
+```
+
+## 部分Repository代码
+
+### StudentRepository.java
+
+```java
+/**
+ *
+ * 学生节点Repository
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:05
+ */
+public interface StudentRepository extends Neo4jRepository {
+ /**
+ * 根据名称查找学生
+ *
+ * @param name 姓名
+ * @param depth 深度
+ * @return 学生信息
+ */
+ Optional findByName(String name, @Depth int depth);
+
+ /**
+ * 根据班级查询班级人数
+ *
+ * @param className 班级名称
+ * @return 班级人数
+ */
+ @Query("MATCH (s:Student)-[r:R_STUDENT_OF_CLASS]->(c:Class{name:{className}}) return count(s)")
+ Long countByClassName(@Param("className") String className);
+
+
+ /**
+ * 查询满足 (学生)-[选课关系]-(课程)-[选课关系]-(学生) 关系的 同学
+ *
+ * @return 返回同学关系
+ */
+ @Query("match (s:Student)-[:R_LESSON_OF_STUDENT]->(l:Lesson)<-[:R_LESSON_OF_STUDENT]-(:Student) with l.name as lessonName,collect(distinct s) as students return lessonName,students")
+ List findByClassmateGroupByLesson();
+
+ /**
+ * 查询师生关系,(学生)-[班级学生关系]-(班级)-[班主任关系]-(教师)
+ *
+ * @return 返回师生关系
+ */
+ @Query("match (s:Student)-[:R_STUDENT_OF_CLASS]->(:Class)-[:R_BOSS_OF_CLASS]->(t:Teacher) with t.name as teacherName,collect(distinct s) as students return teacherName,students")
+ List findTeacherStudentByClass();
+
+ /**
+ * 查询师生关系,(学生)-[选课关系]-(课程)-[任教老师关系]-(教师)
+ *
+ * @return 返回师生关系
+ */
+ @Query("match ((s:Student)-[:R_LESSON_OF_STUDENT]->(:Lesson)-[:R_TEACHER_OF_LESSON]->(t:Teacher))with t.name as teacherName,collect(distinct s) as students return teacherName,students")
+ List findTeacherStudentByLesson();
+}
+```
+
+## Neo4jTest.java
+
+```java
+/**
+ *
+ * 测试Neo4j
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:17
+ */
+@Slf4j
+public class Neo4jTest extends SpringBootDemoNeo4jApplicationTests {
+ @Autowired
+ private NeoService neoService;
+
+ /**
+ * 测试保存
+ */
+ @Test
+ public void testSave() {
+ neoService.initData();
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void testDelete() {
+ neoService.delete();
+ }
+
+ /**
+ * 测试查询 鸣人 学了哪些课程
+ */
+ @Test
+ public void testFindLessonsByStudent() {
+ // 深度为1,则课程的任教老师的属性为null
+ // 深度为2,则会把课程的任教老师的属性赋值
+ List lessons = neoService.findLessonsFromStudent("漩涡鸣人", 2);
+
+ lessons.forEach(lesson -> log.info("【lesson】= {}", JSONUtil.toJsonStr(lesson)));
+ }
+
+ /**
+ * 测试查询班级人数
+ */
+ @Test
+ public void testCountStudent() {
+ Long all = neoService.studentCount(null);
+ log.info("【全校人数】= {}", all);
+ Long seven = neoService.studentCount("第七班");
+ log.info("【第七班人数】= {}", seven);
+ }
+
+ /**
+ * 测试根据课程查询同学关系
+ */
+ @Test
+ public void testFindClassmates() {
+ Map> classmates = neoService.findClassmatesGroupByLesson();
+ classmates.forEach((k, v) -> log.info("因为一起上了【{}】这门课,成为同学关系的有:{}", k, JSONUtil.toJsonStr(v.stream()
+ .map(Student::getName)
+ .collect(Collectors.toList()))));
+ }
+
+ /**
+ * 查询所有师生关系,包括班主任/学生,任课老师/学生
+ */
+ @Test
+ public void testFindTeacherStudent() {
+ Map> teacherStudent = neoService.findTeacherStudent();
+ teacherStudent.forEach((k, v) -> log.info("【{}】教的学生有 {}", k, JSONUtil.toJsonStr(v.stream()
+ .map(Student::getName)
+ .collect(Collectors.toList()))));
+ }
+}
+```
+
+## 截图
+
+运行测试类之后,可以通过访问 http://localhost:7474 ,查看neo里所有节点和关系
+
+![image-20181225150513101](http://static.xkcoding.com/spring-boot-demo/neo4j/063605.jpg)
+
+
+
+## 参考
+
+- spring-data-neo4j 官方文档:https://docs.spring.io/spring-data/neo4j/docs/5.1.2.RELEASE/reference/html/
+- neo4j 官方文档:https://neo4j.com/docs/getting-started/3.5/
diff --git a/demo-neo4j/pom.xml b/demo-neo4j/pom.xml
new file mode 100644
index 0000000..bf1b63f
--- /dev/null
+++ b/demo-neo4j/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ demo-neo4j
+ 1.0.0-SNAPSHOT
+ jar
+
+ demo-neo4j
+ Demo project for Spring Boot
+
+
+ com.xkcoding
+ spring-boot-demo
+ 1.0.0-SNAPSHOT
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-neo4j
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+ com.google.guava
+ guava
+
+
+
+
+ demo-neo4j
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplication.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplication.java
new file mode 100644
index 0000000..45bc870
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplication.java
@@ -0,0 +1,20 @@
+package com.xkcoding.neo4j;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-22 23:50
+ */
+@SpringBootApplication
+public class SpringBootDemoNeo4jApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoNeo4jApplication.class, args);
+ }
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/config/CustomIdStrategy.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/config/CustomIdStrategy.java
new file mode 100644
index 0000000..511236b
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/config/CustomIdStrategy.java
@@ -0,0 +1,19 @@
+package com.xkcoding.neo4j.config;
+
+import cn.hutool.core.util.IdUtil;
+import org.neo4j.ogm.id.IdStrategy;
+
+/**
+ *
+ * 自定义主键策略
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:40
+ */
+public class CustomIdStrategy implements IdStrategy {
+ @Override
+ public Object generateId(Object o) {
+ return IdUtil.fastUUID();
+ }
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/constants/NeoConsts.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/constants/NeoConsts.java
new file mode 100644
index 0000000..b420272
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/constants/NeoConsts.java
@@ -0,0 +1,32 @@
+package com.xkcoding.neo4j.constants;
+
+/**
+ *
+ * 常量池
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:45
+ */
+public interface NeoConsts {
+ /**
+ * 关系:班级拥有的学生
+ */
+ String R_STUDENT_OF_CLASS = "R_STUDENT_OF_CLASS";
+
+ /**
+ * 关系:班级的班主任
+ */
+ String R_BOSS_OF_CLASS = "R_BOSS_OF_CLASS";
+
+ /**
+ * 关系:课程的老师
+ */
+ String R_TEACHER_OF_LESSON = "R_TEACHER_OF_LESSON";
+
+ /**
+ * 关系:学生选的课
+ */
+ String R_LESSON_OF_STUDENT = "R_LESSON_OF_STUDENT";
+
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Class.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Class.java
new file mode 100644
index 0000000..faf5835
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Class.java
@@ -0,0 +1,45 @@
+package com.xkcoding.neo4j.model;
+
+import com.xkcoding.neo4j.config.CustomIdStrategy;
+import com.xkcoding.neo4j.constants.NeoConsts;
+import lombok.*;
+import org.neo4j.ogm.annotation.GeneratedValue;
+import org.neo4j.ogm.annotation.Id;
+import org.neo4j.ogm.annotation.NodeEntity;
+import org.neo4j.ogm.annotation.Relationship;
+
+/**
+ *
+ * 班级节点
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:44
+ */
+@Data
+@NoArgsConstructor
+@RequiredArgsConstructor(staticName = "of")
+@AllArgsConstructor
+@Builder
+@NodeEntity
+public class Class {
+ /**
+ * 主键
+ */
+ @Id
+ @GeneratedValue(strategy = CustomIdStrategy.class)
+ private String id;
+
+ /**
+ * 班级名称
+ */
+ @NonNull
+ private String name;
+
+ /**
+ * 班级的班主任
+ */
+ @Relationship(NeoConsts.R_BOSS_OF_CLASS)
+ @NonNull
+ private Teacher boss;
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Lesson.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Lesson.java
new file mode 100644
index 0000000..6fa9f43
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Lesson.java
@@ -0,0 +1,45 @@
+package com.xkcoding.neo4j.model;
+
+import com.xkcoding.neo4j.config.CustomIdStrategy;
+import com.xkcoding.neo4j.constants.NeoConsts;
+import lombok.*;
+import org.neo4j.ogm.annotation.GeneratedValue;
+import org.neo4j.ogm.annotation.Id;
+import org.neo4j.ogm.annotation.NodeEntity;
+import org.neo4j.ogm.annotation.Relationship;
+
+/**
+ *
+ * 课程节点
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:55
+ */
+@Data
+@NoArgsConstructor
+@RequiredArgsConstructor(staticName = "of")
+@AllArgsConstructor
+@Builder
+@NodeEntity
+public class Lesson {
+ /**
+ * 主键,自定义主键策略,使用UUID生成
+ */
+ @Id
+ @GeneratedValue(strategy = CustomIdStrategy.class)
+ private String id;
+
+ /**
+ * 课程名称
+ */
+ @NonNull
+ private String name;
+
+ /**
+ * 任教老师
+ */
+ @Relationship(NeoConsts.R_TEACHER_OF_LESSON)
+ @NonNull
+ private Teacher teacher;
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Student.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Student.java
new file mode 100644
index 0000000..56dd4f7
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Student.java
@@ -0,0 +1,55 @@
+package com.xkcoding.neo4j.model;
+
+import com.xkcoding.neo4j.config.CustomIdStrategy;
+import com.xkcoding.neo4j.constants.NeoConsts;
+import lombok.*;
+import org.neo4j.ogm.annotation.GeneratedValue;
+import org.neo4j.ogm.annotation.Id;
+import org.neo4j.ogm.annotation.NodeEntity;
+import org.neo4j.ogm.annotation.Relationship;
+
+import java.util.List;
+
+/**
+ *
+ * 学生节点
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:38
+ */
+@Data
+@NoArgsConstructor
+@RequiredArgsConstructor(staticName = "of")
+@AllArgsConstructor
+@Builder
+@NodeEntity
+public class Student {
+ /**
+ * 主键,自定义主键策略,使用UUID生成
+ */
+ @Id
+ @GeneratedValue(strategy = CustomIdStrategy.class)
+ private String id;
+
+ /**
+ * 学生姓名
+ */
+ @NonNull
+ private String name;
+
+ /**
+ * 学生选的所有课程
+ */
+ @Relationship(NeoConsts.R_LESSON_OF_STUDENT)
+ @NonNull
+ private List lessons;
+
+ /**
+ * 学生所在班级
+ */
+ @Relationship(NeoConsts.R_STUDENT_OF_CLASS)
+ @NonNull
+ private Class clazz;
+
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Teacher.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Teacher.java
new file mode 100644
index 0000000..62106ed
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Teacher.java
@@ -0,0 +1,36 @@
+package com.xkcoding.neo4j.model;
+
+import com.xkcoding.neo4j.config.CustomIdStrategy;
+import lombok.*;
+import org.neo4j.ogm.annotation.GeneratedValue;
+import org.neo4j.ogm.annotation.Id;
+import org.neo4j.ogm.annotation.NodeEntity;
+
+/**
+ *
+ * 教师节点
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 14:54
+ */
+@Data
+@NoArgsConstructor
+@RequiredArgsConstructor(staticName = "of")
+@AllArgsConstructor
+@Builder
+@NodeEntity
+public class Teacher {
+ /**
+ * 主键,自定义主键策略,使用UUID生成
+ */
+ @Id
+ @GeneratedValue(strategy = CustomIdStrategy.class)
+ private String id;
+
+ /**
+ * 教师姓名
+ */
+ @NonNull
+ private String name;
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/ClassmateInfoGroupByLesson.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/ClassmateInfoGroupByLesson.java
new file mode 100644
index 0000000..453d11d
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/ClassmateInfoGroupByLesson.java
@@ -0,0 +1,29 @@
+package com.xkcoding.neo4j.payload;
+
+import com.xkcoding.neo4j.model.Student;
+import lombok.Data;
+import org.springframework.data.neo4j.annotation.QueryResult;
+
+import java.util.List;
+
+/**
+ *
+ * 按照课程分组的同学关系
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 19:18
+ */
+@Data
+@QueryResult
+public class ClassmateInfoGroupByLesson {
+ /**
+ * 课程名称
+ */
+ private String lessonName;
+
+ /**
+ * 学生信息
+ */
+ private List students;
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/TeacherStudent.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/TeacherStudent.java
new file mode 100644
index 0000000..d70fcf2
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/TeacherStudent.java
@@ -0,0 +1,29 @@
+package com.xkcoding.neo4j.payload;
+
+import com.xkcoding.neo4j.model.Student;
+import lombok.Data;
+import org.springframework.data.neo4j.annotation.QueryResult;
+
+import java.util.List;
+
+/**
+ *
+ * 师生关系
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 19:18
+ */
+@Data
+@QueryResult
+public class TeacherStudent {
+ /**
+ * 教师姓名
+ */
+ private String teacherName;
+
+ /**
+ * 学生信息
+ */
+ private List students;
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/ClassRepository.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/ClassRepository.java
new file mode 100644
index 0000000..ef85979
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/ClassRepository.java
@@ -0,0 +1,24 @@
+package com.xkcoding.neo4j.repository;
+
+import com.xkcoding.neo4j.model.Class;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+
+import java.util.Optional;
+
+/**
+ *
+ * 班级节点Repository
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:05
+ */
+public interface ClassRepository extends Neo4jRepository {
+ /**
+ * 根据班级名称查询班级信息
+ *
+ * @param name 班级名称
+ * @return 班级信息
+ */
+ Optional findByName(String name);
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/LessonRepository.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/LessonRepository.java
new file mode 100644
index 0000000..fcf010c
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/LessonRepository.java
@@ -0,0 +1,15 @@
+package com.xkcoding.neo4j.repository;
+
+import com.xkcoding.neo4j.model.Lesson;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+
+/**
+ *
+ * 课程节点Repository
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:05
+ */
+public interface LessonRepository extends Neo4jRepository {
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/StudentRepository.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/StudentRepository.java
new file mode 100644
index 0000000..a5037c9
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/StudentRepository.java
@@ -0,0 +1,65 @@
+package com.xkcoding.neo4j.repository;
+
+import com.xkcoding.neo4j.model.Student;
+import com.xkcoding.neo4j.payload.ClassmateInfoGroupByLesson;
+import com.xkcoding.neo4j.payload.TeacherStudent;
+import org.springframework.data.neo4j.annotation.Depth;
+import org.springframework.data.neo4j.annotation.Query;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ *
+ * 学生节点Repository
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:05
+ */
+public interface StudentRepository extends Neo4jRepository {
+ /**
+ * 根据名称查找学生
+ *
+ * @param name 姓名
+ * @param depth 深度
+ * @return 学生信息
+ */
+ Optional findByName(String name, @Depth int depth);
+
+ /**
+ * 根据班级查询班级人数
+ *
+ * @param className 班级名称
+ * @return 班级人数
+ */
+ @Query("MATCH (s:Student)-[r:R_STUDENT_OF_CLASS]->(c:Class{name:{className}}) return count(s)")
+ Long countByClassName(@Param("className") String className);
+
+
+ /**
+ * 查询满足 (学生)-[选课关系]-(课程)-[选课关系]-(学生) 关系的 同学
+ *
+ * @return 返回同学关系
+ */
+ @Query("match (s:Student)-[:R_LESSON_OF_STUDENT]->(l:Lesson)<-[:R_LESSON_OF_STUDENT]-(:Student) with l.name as lessonName,collect(distinct s) as students return lessonName,students")
+ List findByClassmateGroupByLesson();
+
+ /**
+ * 查询师生关系,(学生)-[班级学生关系]-(班级)-[班主任关系]-(教师)
+ *
+ * @return 返回师生关系
+ */
+ @Query("match (s:Student)-[:R_STUDENT_OF_CLASS]->(:Class)-[:R_BOSS_OF_CLASS]->(t:Teacher) with t.name as teacherName,collect(distinct s) as students return teacherName,students")
+ List findTeacherStudentByClass();
+
+ /**
+ * 查询师生关系,(学生)-[选课关系]-(课程)-[任教老师关系]-(教师)
+ *
+ * @return 返回师生关系
+ */
+ @Query("match ((s:Student)-[:R_LESSON_OF_STUDENT]->(:Lesson)-[:R_TEACHER_OF_LESSON]->(t:Teacher))with t.name as teacherName,collect(distinct s) as students return teacherName,students")
+ List findTeacherStudentByLesson();
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/TeacherRepository.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/TeacherRepository.java
new file mode 100644
index 0000000..f7d2d64
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/TeacherRepository.java
@@ -0,0 +1,15 @@
+package com.xkcoding.neo4j.repository;
+
+import com.xkcoding.neo4j.model.Teacher;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+
+/**
+ *
+ * 教师节点Repository
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:05
+ */
+public interface TeacherRepository extends Neo4jRepository {
+}
diff --git a/demo-neo4j/src/main/java/com/xkcoding/neo4j/service/NeoService.java b/demo-neo4j/src/main/java/com/xkcoding/neo4j/service/NeoService.java
new file mode 100644
index 0000000..01c84c7
--- /dev/null
+++ b/demo-neo4j/src/main/java/com/xkcoding/neo4j/service/NeoService.java
@@ -0,0 +1,177 @@
+package com.xkcoding.neo4j.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.xkcoding.neo4j.model.Class;
+import com.xkcoding.neo4j.model.Lesson;
+import com.xkcoding.neo4j.model.Student;
+import com.xkcoding.neo4j.model.Teacher;
+import com.xkcoding.neo4j.payload.ClassmateInfoGroupByLesson;
+import com.xkcoding.neo4j.payload.TeacherStudent;
+import com.xkcoding.neo4j.repository.ClassRepository;
+import com.xkcoding.neo4j.repository.LessonRepository;
+import com.xkcoding.neo4j.repository.StudentRepository;
+import com.xkcoding.neo4j.repository.TeacherRepository;
+import org.neo4j.ogm.session.Session;
+import org.neo4j.ogm.session.SessionFactory;
+import org.neo4j.ogm.transaction.Transaction;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *
+ * NeoService
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:19
+ */
+@Service
+public class NeoService {
+ @Autowired
+ private ClassRepository classRepo;
+
+ @Autowired
+ private LessonRepository lessonRepo;
+
+ @Autowired
+ private StudentRepository studentRepo;
+
+ @Autowired
+ private TeacherRepository teacherRepo;
+
+ @Autowired
+ private SessionFactory sessionFactory;
+
+ /**
+ * 初始化数据
+ */
+ @Transactional
+ public void initData() {
+ // 初始化老师
+ Teacher akai = Teacher.of("迈特凯");
+ Teacher kakaxi = Teacher.of("旗木卡卡西");
+ Teacher zilaiye = Teacher.of("自来也");
+ Teacher gangshou = Teacher.of("纲手");
+ Teacher dashewan = Teacher.of("大蛇丸");
+ teacherRepo.save(akai);
+ teacherRepo.save(kakaxi);
+ teacherRepo.save(zilaiye);
+ teacherRepo.save(gangshou);
+ teacherRepo.save(dashewan);
+
+ // 初始化课程
+ Lesson tishu = Lesson.of("体术", akai);
+ Lesson huanshu = Lesson.of("幻术", kakaxi);
+ Lesson shoulijian = Lesson.of("手里剑", kakaxi);
+ Lesson luoxuanwan = Lesson.of("螺旋丸", zilaiye);
+ Lesson xianshu = Lesson.of("仙术", zilaiye);
+ Lesson yiliao = Lesson.of("医疗", gangshou);
+ Lesson zhouyin = Lesson.of("咒印", dashewan);
+ lessonRepo.save(tishu);
+ lessonRepo.save(huanshu);
+ lessonRepo.save(shoulijian);
+ lessonRepo.save(luoxuanwan);
+ lessonRepo.save(xianshu);
+ lessonRepo.save(yiliao);
+ lessonRepo.save(zhouyin);
+
+ // 初始化班级
+ Class three = Class.of("第三班", akai);
+ Class seven = Class.of("第七班", kakaxi);
+ classRepo.save(three);
+ classRepo.save(seven);
+
+ // 初始化学生
+ List threeClass = Lists.newArrayList(Student.of("漩涡鸣人", Lists.newArrayList(tishu, shoulijian, luoxuanwan, xianshu), seven), Student.of("宇智波佐助", Lists.newArrayList(huanshu, zhouyin, shoulijian), seven), Student.of("春野樱", Lists.newArrayList(tishu, yiliao, shoulijian), seven));
+ List sevenClass = Lists.newArrayList(Student.of("李洛克", Lists.newArrayList(tishu), three), Student.of("日向宁次", Lists.newArrayList(tishu), three), Student.of("天天", Lists.newArrayList(tishu), three));
+
+ studentRepo.saveAll(threeClass);
+ studentRepo.saveAll(sevenClass);
+
+ }
+
+ /**
+ * 删除数据
+ */
+ @Transactional
+ public void delete() {
+ // 使用语句删除
+ Session session = sessionFactory.openSession();
+ Transaction transaction = session.beginTransaction();
+ session.query("match (n)-[r]-() delete n,r", Maps.newHashMap());
+ session.query("match (n)-[r]-() delete r", Maps.newHashMap());
+ session.query("match (n) delete n", Maps.newHashMap());
+ transaction.commit();
+
+ // 使用 repository 删除
+ studentRepo.deleteAll();
+ classRepo.deleteAll();
+ lessonRepo.deleteAll();
+ teacherRepo.deleteAll();
+ }
+
+ /**
+ * 根据学生姓名查询所选课程
+ *
+ * @param studentName 学生姓名
+ * @param depth 深度
+ * @return 课程列表
+ */
+ public List findLessonsFromStudent(String studentName, int depth) {
+ List lessons = Lists.newArrayList();
+ studentRepo.findByName(studentName, depth).ifPresent(student -> lessons.addAll(student.getLessons()));
+ return lessons;
+ }
+
+ /**
+ * 查询全校学生数
+ *
+ * @return 学生总数
+ */
+ public Long studentCount(String className) {
+ if (StrUtil.isBlank(className)) {
+ return studentRepo.count();
+ } else {
+ return studentRepo.countByClassName(className);
+ }
+ }
+
+ /**
+ * 查询同学关系,根据课程
+ *
+ * @return 返回同学关系
+ */
+ public Map> findClassmatesGroupByLesson() {
+ List groupByLesson = studentRepo.findByClassmateGroupByLesson();
+ Map> result = Maps.newHashMap();
+
+ groupByLesson.forEach(classmateInfoGroupByLesson -> result.put(classmateInfoGroupByLesson.getLessonName(), classmateInfoGroupByLesson.getStudents()));
+
+ return result;
+ }
+
+ /**
+ * 查询所有师生关系,包括班主任/学生,任课老师/学生
+ *
+ * @return 师生关系
+ */
+ public Map> findTeacherStudent() {
+ List teacherStudentByClass = studentRepo.findTeacherStudentByClass();
+ List teacherStudentByLesson = studentRepo.findTeacherStudentByLesson();
+ Map> result = Maps.newHashMap();
+
+ teacherStudentByClass.forEach(teacherStudent -> result.put(teacherStudent.getTeacherName(), Sets.newHashSet(teacherStudent.getStudents())));
+
+ teacherStudentByLesson.forEach(teacherStudent -> result.put(teacherStudent.getTeacherName(), Sets.newHashSet(teacherStudent.getStudents())));
+
+ return result;
+ }
+}
diff --git a/spring-boot-demo-neo4j/src/main/resources/application.yml b/demo-neo4j/src/main/resources/application.yml
similarity index 100%
rename from spring-boot-demo-neo4j/src/main/resources/application.yml
rename to demo-neo4j/src/main/resources/application.yml
diff --git a/demo-neo4j/src/test/java/com/xkcoding/neo4j/Neo4jTest.java b/demo-neo4j/src/test/java/com/xkcoding/neo4j/Neo4jTest.java
new file mode 100644
index 0000000..c7cc939
--- /dev/null
+++ b/demo-neo4j/src/test/java/com/xkcoding/neo4j/Neo4jTest.java
@@ -0,0 +1,85 @@
+package com.xkcoding.neo4j;
+
+import cn.hutool.json.JSONUtil;
+import com.xkcoding.neo4j.model.Lesson;
+import com.xkcoding.neo4j.model.Student;
+import com.xkcoding.neo4j.service.NeoService;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 测试Neo4j
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2018-12-24 15:17
+ */
+@Slf4j
+public class Neo4jTest extends SpringBootDemoNeo4jApplicationTests {
+ @Autowired
+ private NeoService neoService;
+
+ /**
+ * 测试保存
+ */
+ @Test
+ public void testSave() {
+ neoService.initData();
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void testDelete() {
+ neoService.delete();
+ }
+
+ /**
+ * 测试查询 鸣人 学了哪些课程
+ */
+ @Test
+ public void testFindLessonsByStudent() {
+ // 深度为1,则课程的任教老师的属性为null
+ // 深度为2,则会把课程的任教老师的属性赋值
+ List lessons = neoService.findLessonsFromStudent("漩涡鸣人", 2);
+
+ lessons.forEach(lesson -> log.info("【lesson】= {}", JSONUtil.toJsonStr(lesson)));
+ }
+
+ /**
+ * 测试查询班级人数
+ */
+ @Test
+ public void testCountStudent() {
+ Long all = neoService.studentCount(null);
+ log.info("【全校人数】= {}", all);
+ Long seven = neoService.studentCount("第七班");
+ log.info("【第七班人数】= {}", seven);
+ }
+
+ /**
+ * 测试根据课程查询同学关系
+ */
+ @Test
+ public void testFindClassmates() {
+ Map> classmates = neoService.findClassmatesGroupByLesson();
+ classmates.forEach((k, v) -> log.info("因为一起上了【{}】这门课,成为同学关系的有:{}", k, JSONUtil.toJsonStr(v.stream().map(Student::getName).collect(Collectors.toList()))));
+ }
+
+ /**
+ * 查询所有师生关系,包括班主任/学生,任课老师/学生
+ */
+ @Test
+ public void testFindTeacherStudent() {
+ Map> teacherStudent = neoService.findTeacherStudent();
+ teacherStudent.forEach((k, v) -> log.info("【{}】教的学生有 {}", k, JSONUtil.toJsonStr(v.stream().map(Student::getName).collect(Collectors.toList()))));
+ }
+}
diff --git a/spring-boot-demo-neo4j/src/test/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplicationTests.java b/demo-neo4j/src/test/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplicationTests.java
similarity index 100%
rename from spring-boot-demo-neo4j/src/test/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplicationTests.java
rename to demo-neo4j/src/test/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplicationTests.java
diff --git a/spring-boot-demo-oauth/.gitignore b/demo-oauth/.gitignore
similarity index 100%
rename from spring-boot-demo-oauth/.gitignore
rename to demo-oauth/.gitignore
diff --git a/spring-boot-demo-oauth/README.md b/demo-oauth/README.md
similarity index 100%
rename from spring-boot-demo-oauth/README.md
rename to demo-oauth/README.md
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc b/demo-oauth/oauth-authorization-server/README.adoc
similarity index 100%
rename from spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc
rename to demo-oauth/oauth-authorization-server/README.adoc
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png b/demo-oauth/oauth-authorization-server/image/Code.png
similarity index 100%
rename from spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png
rename to demo-oauth/oauth-authorization-server/image/Code.png
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png b/demo-oauth/oauth-authorization-server/image/Confirm.png
similarity index 100%
rename from spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png
rename to demo-oauth/oauth-authorization-server/image/Confirm.png
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png b/demo-oauth/oauth-authorization-server/image/Login.png
similarity index 100%
rename from spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png
rename to demo-oauth/oauth-authorization-server/image/Login.png
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png b/demo-oauth/oauth-authorization-server/image/Logout.png
similarity index 100%
rename from spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png
rename to demo-oauth/oauth-authorization-server/image/Logout.png
diff --git a/demo-oauth/oauth-authorization-server/pom.xml b/demo-oauth/oauth-authorization-server/pom.xml
new file mode 100644
index 0000000..e1b5bee
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+ demo-oauth
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ oauth-authorization-server
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.security.oauth.boot
+ spring-security-oauth2-autoconfigure
+ ${spring.boot.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
new file mode 100644
index 0000000..af2dd99
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
@@ -0,0 +1,22 @@
+package com.xkcoding.oauth;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * 启动器
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-02-17 23:52
+ */
+@SpringBootApplication
+public class SpringBootDemoOauthApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoOauthApplication.class, args);
+ }
+
+}
+
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java
new file mode 100644
index 0000000..8fe249d
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java
@@ -0,0 +1,29 @@
+package com.xkcoding.oauth.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+
+/**
+ * 登录失败处理器,失败后携带失败信息重定向到登录地址重新登录.
+ *
+ * @author EchoCow
+ * @date 2020-01-07 13:01
+ */
+@Slf4j
+@Component
+public class ClientLoginFailureHandler implements AuthenticationFailureHandler {
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
+ log.debug("Login failed!");
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.sendRedirect("/oauth/login?error=" + URLEncoder.encode(exception.getLocalizedMessage(), "UTF-8"));
+ }
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java
new file mode 100644
index 0000000..61f9f35
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java
@@ -0,0 +1,30 @@
+package com.xkcoding.oauth.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 客户团退出登录成功处理器.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 22:11
+ */
+@Slf4j
+@Component
+public class ClientLogoutSuccessHandler implements LogoutSuccessHandler {
+
+ @Override
+ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+ response.setStatus(HttpStatus.FOUND.value());
+ // 跳转到客户端的回调地址
+ response.sendRedirect(request.getParameter("redirectUrl"));
+ }
+
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java
new file mode 100644
index 0000000..228ec84
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java
@@ -0,0 +1,51 @@
+package com.xkcoding.oauth.config;
+
+import com.xkcoding.oauth.service.SysClientDetailsService;
+import com.xkcoding.oauth.service.SysUserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
+import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+
+/**
+ * .
+ *
+ * @author EchoCow
+ * @date 2020-01-06 13:32
+ */
+@Configuration
+@RequiredArgsConstructor
+@EnableAuthorizationServer
+public class Oauth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
+ private final SysClientDetailsService sysClientDetailsService;
+ private final SysUserService sysUserService;
+ private final TokenStore tokenStore;
+ private final AuthenticationManager authenticationManager;
+ private final JwtAccessTokenConverter jwtAccessTokenConverter;
+
+ @Override
+ public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
+ endpoints.authenticationManager(authenticationManager).userDetailsService(sysUserService).tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
+ }
+
+ @Override
+ public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
+ // 从数据库读取我们自定义的客户端信息
+ clients.withClientDetails(sysClientDetailsService);
+ }
+
+ @Override
+ public void configure(AuthorizationServerSecurityConfigurer security) {
+ security
+ // 获取 token key 需要进行 basic 认证客户端信息
+ .tokenKeyAccess("isAuthenticated()")
+ // 获取 token 信息同样需要 basic 认证客户端信息
+ .checkTokenAccess("isAuthenticated()");
+ }
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java
new file mode 100644
index 0000000..c002434
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java
@@ -0,0 +1,74 @@
+package com.xkcoding.oauth.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
+
+import java.security.KeyPair;
+
+/**
+ * token 相关配置.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 13:33
+ */
+@Configuration
+@RequiredArgsConstructor
+public class Oauth2AuthorizationTokenConfig {
+
+ /**
+ * 声明 内存 TokenStore 实现,用来存储 token 相关.
+ * 默认实现有 mysql、redis
+ *
+ * @return InMemoryTokenStore
+ */
+ @Bean
+ @Primary
+ public TokenStore tokenStore() {
+ return new InMemoryTokenStore();
+ }
+
+ /**
+ * jwt 令牌 配置,非对称加密
+ *
+ * @return 转换器
+ */
+ @Bean
+ public JwtAccessTokenConverter jwtAccessTokenConverter() {
+ final JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
+ accessTokenConverter.setKeyPair(keyPair());
+ return accessTokenConverter;
+ }
+
+ /**
+ * 密钥 keyPair.
+ * 可用于生成 jwt / jwk.
+ *
+ * @return keyPair
+ */
+ @Bean
+ public KeyPair keyPair() {
+ KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "123456".toCharArray());
+ return keyStoreKeyFactory.getKeyPair("oauth2");
+ }
+
+ /**
+ * 加密方式,使用 BCrypt.
+ * 参数越大加密次数越多,时间越久.
+ * 默认为 10.
+ *
+ * @return PasswordEncoder
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java
new file mode 100644
index 0000000..c9d2531
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java
@@ -0,0 +1,41 @@
+package com.xkcoding.oauth.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * 安全配置.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 13:27
+ */
+@EnableWebSecurity
+@RequiredArgsConstructor
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ private final ClientLogoutSuccessHandler clientLogoutSuccessHandler;
+ private final ClientLoginFailureHandler clientLoginFailureHandler;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.formLogin().loginPage("/oauth/login").failureHandler(clientLoginFailureHandler).loginProcessingUrl("/authorization/form").and().logout().logoutUrl("/oauth/logout").logoutSuccessHandler(clientLogoutSuccessHandler).and().authorizeRequests().antMatchers("/oauth/**").permitAll().anyRequest().authenticated();
+ }
+
+ /**
+ * 授权管理.
+ *
+ * @return 认证管理对象
+ * @throws Exception 认证异常信息
+ */
+ @Override
+ @Bean
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java
new file mode 100644
index 0000000..f1cc4ed
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java
@@ -0,0 +1,21 @@
+/**
+ * spring security oauth2 的相关配置。
+ * 使用 spring boot oauth2 自动配置。
+ * {@link com.xkcoding.oauth.config.Oauth2AuthorizationServerConfig}
+ * 授权服务器相关的配置,主要设置授权服务器如何读取客户端、用户信息和一些端点配置
+ * 可以在这里配置更多的东西,例如端点映射,token 增强等
+ *
+ * {@link com.xkcoding.oauth.config.Oauth2AuthorizationTokenConfig}
+ * 授权服务器 token 相关的配置,主要设置 jwt、加密方式等信息
+ *
+ * {@link com.xkcoding.oauth.config.ClientLogoutSuccessHandler}
+ * 资源服务器退出以后的处理。在授权码模式中,所有的客户端都需要跳转到授权服务器进行登录
+ * 当登录成功以后跳转到回调地址,如果用户需要登出,也要跳转到授权服务器这里进行登出
+ * 但是 spring security oauth2 似乎并没有这个逻辑。
+ * 所以自己给登出端点加了一个 redirect_url 参数,表示登出成功以后要跳转的地址
+ * 这个处理器就是来完成登出成功以后的跳转操作的。
+ *
+ * @author EchoCow
+ * @date 2020-01-07 9:16
+ */
+package com.xkcoding.oauth.config;
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java
new file mode 100644
index 0000000..a7e26e2
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java
@@ -0,0 +1,43 @@
+package com.xkcoding.oauth.controller;
+
+import org.springframework.security.oauth2.provider.AuthorizationRequest;
+import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.SessionAttributes;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.Map;
+
+/**
+ * 自定义确认授权页面.
+ * 需要注意的是: 不能在代码中 setComplete,因为整个授权流程并没有结束
+ * 我们只是在中途修改了它确认的一些信息而已。
+ *
+ * @author EchoCow
+ * @date 2020-01-06 16:42
+ */
+@Controller
+@SessionAttributes("authorizationRequest")
+public class AuthorizationController {
+
+ /**
+ * 自定义确认授权页面
+ * 当然你也可以使用 {@link AuthorizationEndpoint#setUserApprovalPage(String)} 方法
+ * 进行设置,但是 model 就没有那么灵活了
+ *
+ * @param model model
+ * @return ModelAndView
+ */
+ @GetMapping("/oauth/confirm_access")
+ public ModelAndView getAccessConfirmation(Map model) {
+ AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
+ ModelAndView view = new ModelAndView();
+ view.setViewName("authorization");
+ view.addObject("clientId", authorizationRequest.getClientId());
+ // 传递 scope 过去,Set 集合
+ view.addObject("scopes", authorizationRequest.getScope());
+ return view;
+ }
+
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java
new file mode 100644
index 0000000..a3938fa
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java
@@ -0,0 +1,54 @@
+package com.xkcoding.oauth.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.security.Principal;
+import java.util.Objects;
+
+/**
+ * 页面控制器.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 16:30
+ */
+@Controller
+@RequestMapping("/oauth")
+@RequiredArgsConstructor
+public class Oauth2Controller {
+
+ /**
+ * 授权码模式跳转到登录页面
+ *
+ * @return view
+ */
+ @GetMapping("/login")
+ public String loginView() {
+ return "login";
+ }
+
+ /**
+ * 退出登录
+ *
+ * @param redirectUrl 退出完成后的回调地址
+ * @param principal 用户信息
+ * @return 结果
+ */
+ @GetMapping("/logout")
+ public ModelAndView logoutView(@RequestParam("redirect_url") String redirectUrl, Principal principal) {
+ if (Objects.isNull(principal)) {
+ throw new ResourceAccessException("请求错误,用户尚未登录");
+ }
+ ModelAndView view = new ModelAndView();
+ view.setViewName("logout");
+ view.addObject("user", principal.getName());
+ view.addObject("redirectUrl", redirectUrl);
+ return view;
+ }
+
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java
new file mode 100644
index 0000000..a4c53ec
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java
@@ -0,0 +1,14 @@
+/**
+ * 控制器。除了业务逻辑的以外,提供两个控制器来帮助完成自定义:
+ * {@link com.xkcoding.oauth.controller.AuthorizationController}
+ * 自定义的授权控制器,重新设置到我们的界面中去,不使用他的默认实现
+ *
+ * {@link com.xkcoding.oauth.controller.Oauth2Controller}
+ * 页面跳转的控制器,这里拿出来是因为真的可以做很多事。比如登录的时候携带点什么
+ * 或者退出的时候携带什么标识,都可以。
+ *
+ * @author EchoCow
+ * @date 2020-01-07 11:25
+ * @see org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
+ */
+package com.xkcoding.oauth.controller;
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java
new file mode 100644
index 0000000..7562d28
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java
@@ -0,0 +1,191 @@
+package com.xkcoding.oauth.entity;
+
+import lombok.Data;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.provider.ClientDetails;
+import org.springframework.security.oauth2.provider.client.BaseClientDetails;
+
+import javax.persistence.*;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 客户端信息.
+ * 这里实现了 ClientDetails 接口
+ * 个人建议不应该在实体类里面写任何逻辑代码
+ * 而为了避免实体类耦合严重不应该去实现这个接口的
+ * 但是这里为了演示和 {@link SysUser} 不同的方式,所以就选择实现这个接口了
+ * 另一种方式是写一个方法将它转化为默认实现 {@link BaseClientDetails} 比较好一点并且简单很多
+ *
+ * @author EchoCow
+ * @date 2020-01-06 12:54
+ */
+@Data
+@Table
+@Entity
+public class SysClientDetails implements ClientDetails {
+
+ /**
+ * 主键
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ /**
+ * client id
+ */
+ private String clientId;
+
+ /**
+ * client 密钥
+ */
+ private String clientSecret;
+
+ /**
+ * 资源服务器名称
+ */
+ private String resourceIds;
+
+ /**
+ * 授权域
+ */
+ private String scopes;
+
+ /**
+ * 授权类型
+ */
+ private String grantTypes;
+
+ /**
+ * 重定向地址,授权码时必填
+ */
+ private String redirectUrl;
+
+ /**
+ * 授权信息
+ */
+ private String authorizations;
+
+ /**
+ * 授权令牌有效时间
+ */
+ private Integer accessTokenValiditySeconds;
+
+ /**
+ * 刷新令牌有效时间
+ */
+ private Integer refreshTokenValiditySeconds;
+
+ /**
+ * 自动授权请求域
+ */
+ private String autoApproveScopes;
+
+ /**
+ * 是否安全
+ *
+ * @return 结果
+ */
+ @Override
+ public boolean isSecretRequired() {
+ return this.clientSecret != null;
+ }
+
+ /**
+ * 是否有 scopes
+ *
+ * @return 结果
+ */
+ @Override
+ public boolean isScoped() {
+ return this.scopes != null && !this.scopes.isEmpty();
+ }
+
+ /**
+ * scopes
+ *
+ * @return scopes
+ */
+ @Override
+ public Set getScope() {
+ return stringToSet(scopes);
+ }
+
+ /**
+ * 授权类型
+ *
+ * @return 结果
+ */
+ @Override
+ public Set getAuthorizedGrantTypes() {
+ return stringToSet(grantTypes);
+ }
+
+ @Override
+ public Set getResourceIds() {
+ return stringToSet(resourceIds);
+ }
+
+
+ /**
+ * 获取回调地址
+ *
+ * @return redirectUrl
+ */
+ @Override
+ public Set getRegisteredRedirectUri() {
+ return stringToSet(redirectUrl);
+ }
+
+ /**
+ * 这里需要提一下
+ * 个人觉得这里应该是客户端所有的权限
+ * 但是已经有 scope 的存在可以很好的对客户端的权限进行认证了
+ * 那么在 oauth2 的四个角色中,这里就有可能是资源服务器的权限
+ * 但是一般资源服务器都有自己的权限管理机制,比如拿到用户信息后做 RBAC
+ * 所以在 spring security 的默认实现中直接给的是空的一个集合
+ * 这里我们也给他一个空的把
+ *
+ * @return GrantedAuthority
+ */
+ @Override
+ public Collection getAuthorities() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * 判断是否自动授权
+ *
+ * @param scope scope
+ * @return 结果
+ */
+ @Override
+ public boolean isAutoApprove(String scope) {
+ if (autoApproveScopes == null || autoApproveScopes.isEmpty()) {
+ return false;
+ }
+ Set authorizationSet = stringToSet(authorizations);
+ for (String auto : authorizationSet) {
+ if ("true".equalsIgnoreCase(auto) || scope.matches(auto)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * additional information 是 spring security 的保留字段
+ * 暂时用不到,直接给个空的即可
+ *
+ * @return map
+ */
+ @Override
+ public Map getAdditionalInformation() {
+ return Collections.emptyMap();
+ }
+
+ private Set stringToSet(String s) {
+ return Arrays.stream(s.split(",")).collect(Collectors.toSet());
+ }
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java
new file mode 100644
index 0000000..a5362e6
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java
@@ -0,0 +1,48 @@
+package com.xkcoding.oauth.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.persistence.*;
+import java.util.Set;
+
+/**
+ * 这里完全可以只用一个字段代替的
+ * 但是想了想还是模拟实际的情况来把
+ * 角色信息.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 12:44
+ */
+@Data
+@Table
+@Entity
+@EqualsAndHashCode(exclude = {"users"})
+@ToString(exclude = "users")
+public class SysRole {
+
+ /**
+ * 主键.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ /**
+ * 角色名称,按照 spring security 规范
+ * 需要以 ROLE_ 开头.
+ */
+ private String name;
+
+ /**
+ * 角色描述.
+ */
+ private String description;
+
+ /**
+ * 当前角色所有用户.
+ */
+ @ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
+ private Set users;
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java
new file mode 100644
index 0000000..4a04933
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java
@@ -0,0 +1,52 @@
+package com.xkcoding.oauth.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import javax.persistence.*;
+import java.util.Set;
+
+/**
+ * 用户实体.
+ * 避免实体类耦合,所以不去实现 {@link UserDetails} 接口
+ * 因为有且只有登录加载用户的时候才会需要这个接口
+ * 我们就手动构建一个 {@link User} 的默认实现就可以了
+ * 实现接口的方式可以参考 {@link SysClientDetails}
+ *
+ * @author EchoCow
+ * @date 2020-01-06 12:41
+ */
+@Data
+@Table
+@Entity
+@EqualsAndHashCode(exclude = "roles")
+@ToString(exclude = "roles")
+public class SysUser {
+
+ /**
+ * 主键.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ /**
+ * 用户名.
+ */
+ private String username;
+
+ /**
+ * 密码.
+ */
+ private String password;
+
+ /**
+ * 当前用户所有角色.
+ */
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
+ private Set roles;
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java
new file mode 100644
index 0000000..83dcfd1
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java
@@ -0,0 +1,33 @@
+package com.xkcoding.oauth.repostiory;
+
+import com.xkcoding.oauth.entity.SysClientDetails;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+
+import java.util.Optional;
+
+/**
+ * 客户端信息.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 13:09
+ */
+public interface SysClientDetailsRepository extends JpaRepository {
+
+ /**
+ * 通过 clientId 查找客户端信息.
+ *
+ * @param clientId clientId
+ * @return 结果
+ */
+ Optional findFirstByClientId(String clientId);
+
+ /**
+ * 根据客户端 id 删除客户端
+ *
+ * @param clientId 客户端id
+ */
+ @Modifying
+ void deleteByClientId(String clientId);
+
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java
new file mode 100644
index 0000000..0145759
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java
@@ -0,0 +1,24 @@
+package com.xkcoding.oauth.repostiory;
+
+import com.xkcoding.oauth.entity.SysUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+/**
+ * 用户信息仓库.
+ *
+ * @author EchoCow
+ * @date 2020-01-06 13:08
+ */
+public interface SysUserRepository extends JpaRepository {
+
+ /**
+ * 通过用户名查找用户.
+ *
+ * @param username 用户名
+ * @return 结果
+ */
+ Optional findFirstByUsername(String username);
+
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java
new file mode 100644
index 0000000..5ccbdbd
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java
@@ -0,0 +1,67 @@
+package com.xkcoding.oauth.service;
+
+import com.xkcoding.oauth.entity.SysClientDetails;
+import org.springframework.security.oauth2.provider.ClientAlreadyExistsException;
+import org.springframework.security.oauth2.provider.ClientDetailsService;
+import org.springframework.security.oauth2.provider.ClientRegistrationService;
+import org.springframework.security.oauth2.provider.NoSuchClientException;
+
+import java.util.List;
+
+/**
+ * 声明自己的实现.
+ * 参见 {@link ClientRegistrationService}
+ *
+ * @author EchoCow
+ * @date 2020-01-06 13:39
+ */
+public interface SysClientDetailsService extends ClientDetailsService {
+
+ /**
+ * 通过客户端 id 查询
+ *
+ * @param clientId 客户端 id
+ * @return 结果
+ */
+ SysClientDetails findByClientId(String clientId);
+
+ /**
+ * 添加客户端信息.
+ *
+ * @param clientDetails 客户端信息
+ * @throws ClientAlreadyExistsException 客户端已存在
+ */
+ void addClientDetails(SysClientDetails clientDetails) throws ClientAlreadyExistsException;
+
+ /**
+ * 更新客户端信息,不包括 clientSecret.
+ *
+ * @param clientDetails 客户端信息
+ * @throws NoSuchClientException 找不到客户端异常
+ */
+ void updateClientDetails(SysClientDetails clientDetails) throws NoSuchClientException;
+
+ /**
+ * 更新客户端密钥.
+ *
+ * @param clientId 客户端 id
+ * @param clientSecret 客户端密钥
+ * @throws NoSuchClientException 找不到客户端异常
+ */
+ void updateClientSecret(String clientId, String clientSecret) throws NoSuchClientException;
+
+ /**
+ * 删除客户端信息.
+ *
+ * @param clientId 客户端 id
+ * @throws NoSuchClientException 找不到客户端异常
+ */
+ void removeClientDetails(String clientId) throws NoSuchClientException;
+
+ /**
+ * 查询所有
+ *
+ * @return 结果
+ */
+ List findAll();
+}
diff --git a/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java
new file mode 100644
index 0000000..95b8fa9
--- /dev/null
+++ b/demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java
@@ -0,0 +1,59 @@
+package com.xkcoding.oauth.service;
+
+import com.xkcoding.oauth.entity.SysUser;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+import java.util.List;
+
+
+/**
+ * .
+ *
+ * @author