Repository: xkcoding/spring-boot-demo
Branch: master
Commit: 87a142f9604c
Files: 875
Total size: 2.0 MB
Directory structure:
gitextract_ajnuvaj3/
├── .codacy.yml
├── .editorconfig
├── .gitee/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── maven.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.en.md
├── README.md
├── TODO.en.md
├── TODO.md
├── demo-activiti/
│ ├── .gitignore
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── activiti/
│ │ │ ├── SpringBootDemoActivitiApplication.java
│ │ │ ├── config/
│ │ │ │ └── SecurityConfiguration.java
│ │ │ └── util/
│ │ │ └── SecurityUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── processes/
│ │ └── team01.bpmn
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── activiti/
│ └── SpringBootDemoActivitiApplicationTests.java
├── demo-actuator/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── actuator/
│ │ │ └── SpringBootDemoActuatorApplication.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── actuator/
│ └── SpringBootDemoActuatorApplicationTests.java
├── demo-admin/
│ ├── README.md
│ ├── admin-client/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── pom.xml
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── xkcoding/
│ │ │ │ └── admin/
│ │ │ │ └── client/
│ │ │ │ ├── SpringBootDemoAdminClientApplication.java
│ │ │ │ └── controller/
│ │ │ │ └── IndexController.java
│ │ │ └── resources/
│ │ │ └── application.yml
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── admin/
│ │ └── client/
│ │ └── SpringBootDemoAdminClientApplicationTests.java
│ ├── admin-server/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── pom.xml
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── xkcoding/
│ │ │ │ └── admin/
│ │ │ │ └── server/
│ │ │ │ └── SpringBootDemoAdminServerApplication.java
│ │ │ └── resources/
│ │ │ └── application.yml
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── admin/
│ │ └── server/
│ │ └── SpringBootDemoAdminServerApplicationTests.java
│ └── pom.xml
├── demo-async/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── async/
│ │ │ ├── SpringBootDemoAsyncApplication.java
│ │ │ └── task/
│ │ │ └── TaskFactory.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── async/
│ ├── SpringBootDemoAsyncApplicationTests.java
│ └── task/
│ └── TaskFactoryTest.java
├── demo-cache-ehcache/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── cache/
│ │ │ └── ehcache/
│ │ │ ├── SpringBootDemoCacheEhcacheApplication.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ └── service/
│ │ │ ├── UserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── ehcache.xml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── cache/
│ └── ehcache/
│ ├── SpringBootDemoCacheEhcacheApplicationTests.java
│ └── service/
│ └── UserServiceTest.java
├── demo-cache-redis/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── cache/
│ │ │ └── redis/
│ │ │ ├── SpringBootDemoCacheRedisApplication.java
│ │ │ ├── config/
│ │ │ │ └── RedisConfig.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ └── service/
│ │ │ ├── UserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── cache/
│ └── redis/
│ ├── RedisTest.java
│ ├── SpringBootDemoCacheRedisApplicationTests.java
│ └── service/
│ └── UserServiceTest.java
├── demo-codegen/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── codegen/
│ │ │ ├── SpringBootDemoCodegenApplication.java
│ │ │ ├── common/
│ │ │ │ ├── IResultCode.java
│ │ │ │ ├── PageResult.java
│ │ │ │ ├── R.java
│ │ │ │ └── ResultCode.java
│ │ │ ├── constants/
│ │ │ │ └── GenConstants.java
│ │ │ ├── controller/
│ │ │ │ └── CodeGenController.java
│ │ │ ├── entity/
│ │ │ │ ├── ColumnEntity.java
│ │ │ │ ├── GenConfig.java
│ │ │ │ ├── TableEntity.java
│ │ │ │ └── TableRequest.java
│ │ │ ├── service/
│ │ │ │ ├── CodeGenService.java
│ │ │ │ └── impl/
│ │ │ │ └── CodeGenServiceImpl.java
│ │ │ └── utils/
│ │ │ ├── CodeGenUtil.java
│ │ │ └── DbUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── generator.properties
│ │ ├── jdbc_type.properties
│ │ ├── logback-spring.xml
│ │ ├── static/
│ │ │ ├── index.html
│ │ │ └── libs/
│ │ │ ├── datejs/
│ │ │ │ └── date-zh-CN.js
│ │ │ └── iview/
│ │ │ └── iview.css
│ │ └── template/
│ │ ├── Controller.java.vm
│ │ ├── Entity.java.vm
│ │ ├── Mapper.java.vm
│ │ ├── Mapper.xml.vm
│ │ ├── Service.java.vm
│ │ ├── ServiceImpl.java.vm
│ │ └── api.js.vm
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── codegen/
│ ├── CodeGenServiceTest.java
│ └── SpringBootDemoCodegenApplicationTests.java
├── demo-docker/
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── docker/
│ │ │ ├── SpringBootDemoDockerApplication.java
│ │ │ └── controller/
│ │ │ └── HelloController.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── docker/
│ └── SpringBootDemoDockerApplicationTests.java
├── demo-dubbo/
│ ├── .gitignore
│ ├── README.md
│ ├── dubbo-common/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── dubbo/
│ │ └── common/
│ │ └── service/
│ │ └── HelloService.java
│ ├── dubbo-consumer/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── pom.xml
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── xkcoding/
│ │ │ │ └── dubbo/
│ │ │ │ └── consumer/
│ │ │ │ ├── SpringBootDemoDubboConsumerApplication.java
│ │ │ │ └── controller/
│ │ │ │ └── HelloController.java
│ │ │ └── resources/
│ │ │ └── application.yml
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── dubbo/
│ │ └── consumer/
│ │ └── SpringBootDemoDubboConsumerApplicationTests.java
│ ├── dubbo-provider/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── pom.xml
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── xkcoding/
│ │ │ │ └── dubbo/
│ │ │ │ └── provider/
│ │ │ │ ├── SpringBootDemoDubboProviderApplication.java
│ │ │ │ └── service/
│ │ │ │ └── HelloServiceImpl.java
│ │ │ └── resources/
│ │ │ └── application.yml
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── dubbo/
│ │ └── provider/
│ │ └── SpringBootDemoDubboProviderApplicationTests.java
│ └── pom.xml
├── demo-dynamic-datasource/
│ ├── .gitignore
│ ├── README.md
│ ├── db/
│ │ ├── init.sql
│ │ └── user.sql
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── dynamic/
│ │ │ └── datasource/
│ │ │ ├── SpringBootDemoDynamicDatasourceApplication.java
│ │ │ ├── annotation/
│ │ │ │ └── DefaultDatasource.java
│ │ │ ├── aspect/
│ │ │ │ └── DatasourceSelectorAspect.java
│ │ │ ├── config/
│ │ │ │ ├── DatasourceConfiguration.java
│ │ │ │ ├── MyMapper.java
│ │ │ │ └── MybatisConfiguration.java
│ │ │ ├── controller/
│ │ │ │ ├── DatasourceConfigController.java
│ │ │ │ └── UserController.java
│ │ │ ├── datasource/
│ │ │ │ ├── DatasourceConfigCache.java
│ │ │ │ ├── DatasourceConfigContextHolder.java
│ │ │ │ ├── DatasourceHolder.java
│ │ │ │ ├── DatasourceManager.java
│ │ │ │ ├── DatasourceScheduler.java
│ │ │ │ └── DynamicDataSource.java
│ │ │ ├── mapper/
│ │ │ │ ├── DatasourceConfigMapper.java
│ │ │ │ └── UserMapper.java
│ │ │ ├── model/
│ │ │ │ ├── DatasourceConfig.java
│ │ │ │ └── User.java
│ │ │ └── utils/
│ │ │ └── SpringUtil.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── dynamic/
│ └── datasource/
│ └── SpringBootDemoDynamicDatasourceApplicationTests.java
├── demo-elasticsearch/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── elasticsearch/
│ │ │ ├── SpringBootDemoElasticsearchApplication.java
│ │ │ ├── constants/
│ │ │ │ └── EsConsts.java
│ │ │ ├── model/
│ │ │ │ └── Person.java
│ │ │ └── repository/
│ │ │ └── PersonRepository.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── elasticsearch/
│ ├── SpringBootDemoElasticsearchApplicationTests.java
│ ├── repository/
│ │ └── PersonRepositoryTest.java
│ └── template/
│ └── TemplateTest.java
├── demo-elasticsearch-rest-high-level-client/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── elasticsearch/
│ │ │ ├── ElasticsearchApplication.java
│ │ │ ├── common/
│ │ │ │ ├── Result.java
│ │ │ │ └── ResultCode.java
│ │ │ ├── config/
│ │ │ │ ├── ElasticsearchAutoConfiguration.java
│ │ │ │ └── ElasticsearchProperties.java
│ │ │ ├── contants/
│ │ │ │ └── ElasticsearchConstant.java
│ │ │ ├── exception/
│ │ │ │ └── ElasticsearchException.java
│ │ │ ├── model/
│ │ │ │ └── Person.java
│ │ │ └── service/
│ │ │ ├── PersonService.java
│ │ │ ├── base/
│ │ │ │ └── BaseElasticsearchService.java
│ │ │ └── impl/
│ │ │ └── PersonServiceImpl.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── elasticsearch/
│ └── ElasticsearchApplicationTests.java
├── demo-email/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── email/
│ │ │ ├── SpringBootDemoEmailApplication.java
│ │ │ └── service/
│ │ │ ├── MailService.java
│ │ │ └── impl/
│ │ │ └── MailServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── email/
│ │ │ └── test.html
│ │ └── templates/
│ │ └── welcome.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── email/
│ ├── PasswordTest.java
│ ├── SpringBootDemoEmailApplicationTests.java
│ └── service/
│ └── MailServiceTest.java
├── demo-exception-handler/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── exception/
│ │ │ └── handler/
│ │ │ ├── SpringBootDemoExceptionHandlerApplication.java
│ │ │ ├── constant/
│ │ │ │ └── Status.java
│ │ │ ├── controller/
│ │ │ │ └── TestController.java
│ │ │ ├── exception/
│ │ │ │ ├── BaseException.java
│ │ │ │ ├── JsonException.java
│ │ │ │ └── PageException.java
│ │ │ ├── handler/
│ │ │ │ └── DemoExceptionHandler.java
│ │ │ └── model/
│ │ │ └── ApiResponse.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ └── error.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── exception/
│ └── handler/
│ └── SpringBootDemoExceptionHandlerApplicationTests.java
├── demo-flyway/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── flyway/
│ │ │ └── SpringBootDemoFlywayApplication.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ └── migration/
│ │ ├── V1_0__INIT.sql
│ │ └── V1_1__ALTER.sql
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── AppTest.java
├── demo-graylog/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── graylog/
│ │ │ └── SpringBootDemoGraylogApplication.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── logback-spring.xml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── graylog/
│ └── SpringBootDemoGraylogApplicationTests.java
├── demo-helloworld/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── helloworld/
│ │ │ └── SpringBootDemoHelloworldApplication.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── helloworld/
│ └── SpringBootDemoHelloworldApplicationTests.java
├── demo-https/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── https/
│ │ │ ├── SpringBootDemoHttpsApplication.java
│ │ │ └── config/
│ │ │ └── HttpsConfig.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── server.keystore
│ │ └── static/
│ │ └── index.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── https/
│ └── SpringBootDemoHttpsApplicationTests.java
├── demo-ldap/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── ldap/
│ │ │ ├── LdapDemoApplication.java
│ │ │ ├── api/
│ │ │ │ ├── Result.java
│ │ │ │ └── ResultCode.java
│ │ │ ├── entity/
│ │ │ │ └── Person.java
│ │ │ ├── exception/
│ │ │ │ └── ServiceException.java
│ │ │ ├── repository/
│ │ │ │ └── PersonRepository.java
│ │ │ ├── request/
│ │ │ │ └── LoginRequest.java
│ │ │ ├── service/
│ │ │ │ ├── PersonService.java
│ │ │ │ └── impl/
│ │ │ │ └── PersonServiceImpl.java
│ │ │ └── util/
│ │ │ └── LdapUtils.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── ldap/
│ └── LdapDemoApplicationTests.java
├── demo-log-aop/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── log/
│ │ │ └── aop/
│ │ │ ├── SpringBootDemoLogAopApplication.java
│ │ │ ├── aspectj/
│ │ │ │ └── AopLog.java
│ │ │ └── controller/
│ │ │ └── TestController.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── logback-spring.xml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── log/
│ └── aop/
│ └── SpringBootDemoLogAopApplicationTests.java
├── demo-logback/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── logback/
│ │ │ └── SpringBootDemoLogbackApplication.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── logback-spring.xml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── logback/
│ └── SpringBootDemoLogbackApplicationTests.java
├── demo-mongodb/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── mongodb/
│ │ │ ├── SpringBootDemoMongodbApplication.java
│ │ │ ├── model/
│ │ │ │ └── Article.java
│ │ │ └── repository/
│ │ │ └── ArticleRepository.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── mongodb/
│ ├── SpringBootDemoMongodbApplicationTests.java
│ └── repository/
│ └── ArticleRepositoryTest.java
├── demo-mq-kafka/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── mq/
│ │ │ └── kafka/
│ │ │ ├── SpringBootDemoMqKafkaApplication.java
│ │ │ ├── config/
│ │ │ │ └── KafkaConfig.java
│ │ │ ├── constants/
│ │ │ │ └── KafkaConsts.java
│ │ │ └── handler/
│ │ │ └── MessageHandler.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── mq/
│ └── kafka/
│ └── SpringBootDemoMqKafkaApplicationTests.java
├── demo-mq-rabbitmq/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── mq/
│ │ │ └── rabbitmq/
│ │ │ ├── SpringBootDemoMqRabbitmqApplication.java
│ │ │ ├── config/
│ │ │ │ └── RabbitMqConfig.java
│ │ │ ├── constants/
│ │ │ │ └── RabbitConsts.java
│ │ │ ├── handler/
│ │ │ │ ├── DelayQueueHandler.java
│ │ │ │ ├── DirectQueueOneHandler.java
│ │ │ │ ├── QueueThreeHandler.java
│ │ │ │ └── QueueTwoHandler.java
│ │ │ └── message/
│ │ │ └── MessageStruct.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── mq/
│ └── rabbitmq/
│ └── SpringBootDemoMqRabbitmqApplicationTests.java
├── demo-mq-rocketmq/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── mq/
│ │ │ └── rocketmq/
│ │ │ └── SpringBootDemoMqRocketmqApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── mq/
│ └── rocketmq/
│ └── SpringBootDemoMqRocketmqApplicationTests.java
├── demo-multi-datasource-jpa/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── 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
├── demo-multi-datasource-mybatis/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ ├── sql/
│ │ └── db.sql
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── multi/
│ │ │ └── datasource/
│ │ │ └── mybatis/
│ │ │ ├── SpringBootDemoMultiDatasourceMybatisApplication.java
│ │ │ ├── mapper/
│ │ │ │ └── UserMapper.java
│ │ │ ├── model/
│ │ │ │ └── User.java
│ │ │ └── service/
│ │ │ ├── UserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── multi/
│ └── datasource/
│ └── mybatis/
│ ├── SpringBootDemoMultiDatasourceMybatisApplicationTests.java
│ └── service/
│ └── impl/
│ └── UserServiceImplTest.java
├── demo-neo4j/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── neo4j/
│ │ │ ├── SpringBootDemoNeo4jApplication.java
│ │ │ ├── config/
│ │ │ │ └── CustomIdStrategy.java
│ │ │ ├── constants/
│ │ │ │ └── NeoConsts.java
│ │ │ ├── model/
│ │ │ │ ├── Class.java
│ │ │ │ ├── Lesson.java
│ │ │ │ ├── Student.java
│ │ │ │ └── Teacher.java
│ │ │ ├── payload/
│ │ │ │ ├── ClassmateInfoGroupByLesson.java
│ │ │ │ └── TeacherStudent.java
│ │ │ ├── repository/
│ │ │ │ ├── ClassRepository.java
│ │ │ │ ├── LessonRepository.java
│ │ │ │ ├── StudentRepository.java
│ │ │ │ └── TeacherRepository.java
│ │ │ └── service/
│ │ │ └── NeoService.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── neo4j/
│ ├── Neo4jTest.java
│ └── SpringBootDemoNeo4jApplicationTests.java
├── demo-oauth/
│ ├── .gitignore
│ ├── README.md
│ ├── oauth-authorization-server/
│ │ ├── README.adoc
│ │ ├── pom.xml
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── xkcoding/
│ │ │ │ └── oauth/
│ │ │ │ ├── SpringBootDemoOauthApplication.java
│ │ │ │ ├── config/
│ │ │ │ │ ├── ClientLoginFailureHandler.java
│ │ │ │ │ ├── ClientLogoutSuccessHandler.java
│ │ │ │ │ ├── Oauth2AuthorizationServerConfig.java
│ │ │ │ │ ├── Oauth2AuthorizationTokenConfig.java
│ │ │ │ │ ├── WebSecurityConfig.java
│ │ │ │ │ └── package-info.java
│ │ │ │ ├── controller/
│ │ │ │ │ ├── AuthorizationController.java
│ │ │ │ │ ├── Oauth2Controller.java
│ │ │ │ │ └── package-info.java
│ │ │ │ ├── entity/
│ │ │ │ │ ├── SysClientDetails.java
│ │ │ │ │ ├── SysRole.java
│ │ │ │ │ └── SysUser.java
│ │ │ │ ├── repostiory/
│ │ │ │ │ ├── SysClientDetailsRepository.java
│ │ │ │ │ └── SysUserRepository.java
│ │ │ │ └── service/
│ │ │ │ ├── SysClientDetailsService.java
│ │ │ │ ├── SysUserService.java
│ │ │ │ ├── impl/
│ │ │ │ │ ├── SysClientDetailsServiceImpl.java
│ │ │ │ │ └── SysUserServiceImpl.java
│ │ │ │ └── package-info.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ ├── oauth2.jks
│ │ │ ├── public.txt
│ │ │ └── templates/
│ │ │ ├── authorization.html
│ │ │ ├── common/
│ │ │ │ └── common.html
│ │ │ ├── error.html
│ │ │ ├── login.html
│ │ │ ├── logout.html
│ │ │ └── registerTemplate.html
│ │ └── test/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── oauth/
│ │ │ ├── PasswordEncodeTest.java
│ │ │ ├── oauth/
│ │ │ │ ├── AuthorizationCodeGrantTests.java
│ │ │ │ ├── AuthorizationServerInfo.java
│ │ │ │ └── ResourceOwnerPasswordGrantTests.java
│ │ │ └── repostiory/
│ │ │ ├── SysClientDetailsTest.java
│ │ │ └── SysUserRepositoryTest.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── import.sql
│ │ └── schema.sql
│ ├── oauth-resource-server/
│ │ ├── README.adoc
│ │ ├── pom.xml
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── xkcoding/
│ │ │ │ └── oauth/
│ │ │ │ ├── SpringBootDemoResourceApplication.java
│ │ │ │ ├── config/
│ │ │ │ │ ├── OauthResourceServerConfig.java
│ │ │ │ │ └── OauthResourceTokenConfig.java
│ │ │ │ └── controller/
│ │ │ │ └── TestController.java
│ │ │ └── resources/
│ │ │ └── application.yml
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── oauth/
│ │ ├── AuthorizationTest.java
│ │ └── controller/
│ │ └── TestControllerTest.java
│ └── pom.xml
├── demo-orm-beetlsql/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── orm/
│ │ │ └── beetlsql/
│ │ │ ├── SpringBootDemoOrmBeetlsqlApplication.java
│ │ │ ├── config/
│ │ │ │ └── BeetlConfig.java
│ │ │ ├── dao/
│ │ │ │ └── UserDao.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ └── service/
│ │ │ ├── UserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ ├── data.sql
│ │ └── schema.sql
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── orm/
│ └── beetlsql/
│ ├── SpringBootDemoOrmBeetlsqlApplicationTests.java
│ └── service/
│ └── UserServiceTest.java
├── demo-orm-jdbctemplate/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── orm/
│ │ │ └── jdbctemplate/
│ │ │ ├── SpringBootDemoOrmJdbctemplateApplication.java
│ │ │ ├── annotation/
│ │ │ │ ├── Column.java
│ │ │ │ ├── Ignore.java
│ │ │ │ ├── Pk.java
│ │ │ │ └── Table.java
│ │ │ ├── constant/
│ │ │ │ └── Const.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ ├── dao/
│ │ │ │ ├── UserDao.java
│ │ │ │ └── base/
│ │ │ │ └── BaseDao.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ └── service/
│ │ │ ├── IUserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ ├── data.sql
│ │ └── schema.sql
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── orm/
│ └── jdbctemplate/
│ └── SpringBootDemoOrmJdbctemplateApplicationTests.java
├── demo-orm-jpa/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── orm/
│ │ │ └── jpa/
│ │ │ ├── SpringBootDemoOrmJpaApplication.java
│ │ │ ├── config/
│ │ │ │ └── JpaConfig.java
│ │ │ ├── entity/
│ │ │ │ ├── Department.java
│ │ │ │ ├── User.java
│ │ │ │ └── base/
│ │ │ │ └── AbstractAuditModel.java
│ │ │ └── repository/
│ │ │ ├── DepartmentDao.java
│ │ │ └── UserDao.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ ├── data.sql
│ │ └── schema.sql
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── orm/
│ └── jpa/
│ ├── SpringBootDemoOrmJpaApplicationTests.java
│ └── repository/
│ ├── DepartmentDaoTest.java
│ └── UserDaoTest.java
├── demo-orm-mybatis/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── orm/
│ │ │ └── mybatis/
│ │ │ ├── SpringBootDemoOrmMybatisApplication.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ └── mapper/
│ │ │ └── UserMapper.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── db/
│ │ │ ├── data.sql
│ │ │ └── schema.sql
│ │ └── mappers/
│ │ └── UserMapper.xml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── orm/
│ └── mybatis/
│ ├── SpringBootDemoOrmMybatisApplicationTests.java
│ └── mapper/
│ └── UserMapperTest.java
├── demo-orm-mybatis-mapper-page/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── orm/
│ │ │ └── mybatis/
│ │ │ └── MapperAndPage/
│ │ │ ├── SpringBootDemoOrmMybatisMapperPageApplication.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ └── mapper/
│ │ │ └── UserMapper.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ ├── data.sql
│ │ └── schema.sql
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── orm/
│ └── mybatis/
│ └── MapperAndPage/
│ ├── SpringBootDemoOrmMybatisMapperPageApplicationTests.java
│ └── mapper/
│ └── UserMapperTest.java
├── demo-orm-mybatis-plus/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── orm/
│ │ │ └── mybatis/
│ │ │ └── plus/
│ │ │ ├── SpringBootDemoOrmMybatisPlusApplication.java
│ │ │ ├── config/
│ │ │ │ ├── CommonFieldHandler.java
│ │ │ │ └── MybatisPlusConfig.java
│ │ │ ├── entity/
│ │ │ │ ├── Role.java
│ │ │ │ └── User.java
│ │ │ ├── mapper/
│ │ │ │ ├── RoleMapper.java
│ │ │ │ └── UserMapper.java
│ │ │ └── service/
│ │ │ ├── UserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ ├── data.sql
│ │ └── schema.sql
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── orm/
│ └── mybatis/
│ └── plus/
│ ├── SpringBootDemoOrmMybatisPlusApplicationTests.java
│ ├── activerecord/
│ │ └── ActiveRecordTest.java
│ └── service/
│ └── UserServiceTest.java
├── demo-pay/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── pay/
│ │ │ └── SpringBootDemoPayApplication.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── pay/
│ └── SpringBootDemoPayApplicationTests.java
├── demo-properties/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── properties/
│ │ │ ├── SpringBootDemoPropertiesApplication.java
│ │ │ ├── controller/
│ │ │ │ └── PropertyController.java
│ │ │ └── property/
│ │ │ ├── ApplicationProperty.java
│ │ │ └── DeveloperProperty.java
│ │ └── resources/
│ │ ├── META-INF/
│ │ │ └── additional-spring-configuration-metadata.json
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── properties/
│ └── SpringBootDemoPropertiesApplicationTests.java
├── demo-ratelimit-guava/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── ratelimit/
│ │ │ └── guava/
│ │ │ ├── SpringBootDemoRatelimitGuavaApplication.java
│ │ │ ├── annotation/
│ │ │ │ └── RateLimiter.java
│ │ │ ├── aspect/
│ │ │ │ └── RateLimiterAspect.java
│ │ │ ├── controller/
│ │ │ │ └── TestController.java
│ │ │ └── handler/
│ │ │ └── GlobalExceptionHandler.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── ratelimit/
│ └── guava/
│ └── SpringBootDemoRatelimitGuavaApplicationTests.java
├── demo-ratelimit-redis/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── ratelimit/
│ │ │ └── redis/
│ │ │ ├── SpringBootDemoRatelimitRedisApplication.java
│ │ │ ├── annotation/
│ │ │ │ └── RateLimiter.java
│ │ │ ├── aspect/
│ │ │ │ └── RateLimiterAspect.java
│ │ │ ├── config/
│ │ │ │ └── RedisConfig.java
│ │ │ ├── controller/
│ │ │ │ └── TestController.java
│ │ │ ├── handler/
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ └── util/
│ │ │ └── IpUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── scripts/
│ │ └── redis/
│ │ └── limit.lua
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── ratelimit/
│ └── redis/
│ └── SpringBootDemoRatelimiterRedisApplicationTests.java
├── demo-rbac-security/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ ├── sql/
│ │ └── security.sql
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── rbac/
│ │ │ └── security/
│ │ │ ├── SpringBootDemoRbacSecurityApplication.java
│ │ │ ├── common/
│ │ │ │ ├── ApiResponse.java
│ │ │ │ ├── BaseException.java
│ │ │ │ ├── Consts.java
│ │ │ │ ├── IStatus.java
│ │ │ │ ├── PageResult.java
│ │ │ │ └── Status.java
│ │ │ ├── config/
│ │ │ │ ├── CustomConfig.java
│ │ │ │ ├── IdConfig.java
│ │ │ │ ├── IgnoreConfig.java
│ │ │ │ ├── JwtAuthenticationFilter.java
│ │ │ │ ├── JwtConfig.java
│ │ │ │ ├── RbacAuthorityService.java
│ │ │ │ ├── RedisConfig.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── SecurityHandlerConfig.java
│ │ │ │ └── WebMvcConfig.java
│ │ │ ├── controller/
│ │ │ │ ├── AuthController.java
│ │ │ │ ├── MonitorController.java
│ │ │ │ └── TestController.java
│ │ │ ├── exception/
│ │ │ │ ├── SecurityException.java
│ │ │ │ └── handler/
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ ├── model/
│ │ │ │ ├── Permission.java
│ │ │ │ ├── Role.java
│ │ │ │ ├── RolePermission.java
│ │ │ │ ├── User.java
│ │ │ │ ├── UserRole.java
│ │ │ │ └── unionkey/
│ │ │ │ ├── RolePermissionKey.java
│ │ │ │ └── UserRoleKey.java
│ │ │ ├── payload/
│ │ │ │ ├── LoginRequest.java
│ │ │ │ └── PageCondition.java
│ │ │ ├── repository/
│ │ │ │ ├── PermissionDao.java
│ │ │ │ ├── RoleDao.java
│ │ │ │ ├── RolePermissionDao.java
│ │ │ │ ├── UserDao.java
│ │ │ │ └── UserRoleDao.java
│ │ │ ├── service/
│ │ │ │ ├── CustomUserDetailsService.java
│ │ │ │ └── MonitorService.java
│ │ │ ├── util/
│ │ │ │ ├── JwtUtil.java
│ │ │ │ ├── PageUtil.java
│ │ │ │ ├── RedisUtil.java
│ │ │ │ ├── ResponseUtil.java
│ │ │ │ └── SecurityUtil.java
│ │ │ └── vo/
│ │ │ ├── JwtResponse.java
│ │ │ ├── OnlineUser.java
│ │ │ └── UserPrincipal.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── rbac/
│ └── security/
│ ├── SpringBootDemoRbacSecurityApplicationTests.java
│ ├── repository/
│ │ ├── DataInitTest.java
│ │ └── UserDaoTest.java
│ └── util/
│ └── RedisUtilTest.java
├── demo-rbac-shiro/
│ ├── .gitignore
│ ├── pom.xml
│ ├── sql/
│ │ └── shiro.sql
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── rbac/
│ │ │ └── shiro/
│ │ │ ├── SpringBootDemoRbacShiroApplication.java
│ │ │ ├── common/
│ │ │ │ ├── IResultCode.java
│ │ │ │ ├── R.java
│ │ │ │ └── ResultCode.java
│ │ │ ├── config/
│ │ │ │ └── MybatisPlusConfig.java
│ │ │ └── controller/
│ │ │ └── TestController.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── spy.properties
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── rbac/
│ └── shiro/
│ └── SpringBootDemoRbacShiroApplicationTests.java
├── demo-session/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── session/
│ │ │ ├── SpringBootDemoSessionApplication.java
│ │ │ ├── config/
│ │ │ │ └── WebMvcConfig.java
│ │ │ ├── constants/
│ │ │ │ └── Consts.java
│ │ │ ├── controller/
│ │ │ │ └── PageController.java
│ │ │ └── interceptor/
│ │ │ └── SessionInterceptor.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ ├── index.html
│ │ └── login.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── session/
│ └── SpringBootDemoSessionApplicationTests.java
├── demo-sharding-jdbc/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ ├── sql/
│ │ └── schema.sql
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── sharding/
│ │ │ └── jdbc/
│ │ │ ├── SpringBootDemoShardingJdbcApplication.java
│ │ │ ├── config/
│ │ │ │ ├── CustomSnowflakeKeyGenerator.java
│ │ │ │ └── DataSourceShardingConfig.java
│ │ │ ├── mapper/
│ │ │ │ └── OrderMapper.java
│ │ │ └── model/
│ │ │ └── Order.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── sharding/
│ └── jdbc/
│ └── SpringBootDemoShardingJdbcApplicationTests.java
├── demo-social/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── social/
│ │ │ ├── SpringBootDemoSocialApplication.java
│ │ │ └── controller/
│ │ │ └── OauthController.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── social/
│ └── SpringBootDemoSocialApplicationTests.java
├── demo-swagger/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── swagger/
│ │ │ ├── SpringBootDemoSwaggerApplication.java
│ │ │ ├── common/
│ │ │ │ ├── ApiResponse.java
│ │ │ │ ├── DataType.java
│ │ │ │ └── ParamType.java
│ │ │ ├── config/
│ │ │ │ └── Swagger2Config.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ └── entity/
│ │ │ └── User.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── swagger/
│ └── SpringBootDemoSwaggerApplicationTests.java
├── demo-swagger-beauty/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── swagger/
│ │ │ └── beauty/
│ │ │ ├── SpringBootDemoSwaggerBeautyApplication.java
│ │ │ ├── common/
│ │ │ │ └── ApiResponse.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ └── entity/
│ │ │ └── User.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── swagger/
│ └── beauty/
│ └── SpringBootDemoSwaggerBeautyApplicationTests.java
├── demo-task/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── task/
│ │ │ ├── SpringBootDemoTaskApplication.java
│ │ │ ├── config/
│ │ │ │ └── TaskConfig.java
│ │ │ └── job/
│ │ │ └── TaskJob.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── task/
│ └── SpringBootDemoTaskApplicationTests.java
├── demo-task-quartz/
│ ├── .gitignore
│ ├── README.md
│ ├── init/
│ │ └── dbTables/
│ │ ├── tables_cloudscape.sql
│ │ ├── tables_cubrid.sql
│ │ ├── tables_db2.sql
│ │ ├── tables_db2_v72.sql
│ │ ├── tables_db2_v8.sql
│ │ ├── tables_db2_v95.sql
│ │ ├── tables_derby.sql
│ │ ├── tables_derby_previous.sql
│ │ ├── tables_firebird.sql
│ │ ├── tables_h2.sql
│ │ ├── tables_hsqldb.sql
│ │ ├── tables_hsqldb_old.sql
│ │ ├── tables_informix.sql
│ │ ├── tables_mysql.sql
│ │ ├── tables_mysql_innodb.sql
│ │ ├── tables_oracle.sql
│ │ ├── tables_pointbase.sql
│ │ ├── tables_postgres.sql
│ │ ├── tables_sapdb.sql
│ │ ├── tables_solid.sql
│ │ ├── tables_sqlServer.sql
│ │ └── tables_sybase.sql
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── task/
│ │ │ └── quartz/
│ │ │ ├── SpringBootDemoTaskQuartzApplication.java
│ │ │ ├── common/
│ │ │ │ └── ApiResponse.java
│ │ │ ├── controller/
│ │ │ │ └── JobController.java
│ │ │ ├── entity/
│ │ │ │ ├── domain/
│ │ │ │ │ └── JobAndTrigger.java
│ │ │ │ └── form/
│ │ │ │ └── JobForm.java
│ │ │ ├── job/
│ │ │ │ ├── HelloJob.java
│ │ │ │ ├── TestJob.java
│ │ │ │ └── base/
│ │ │ │ └── BaseJob.java
│ │ │ ├── mapper/
│ │ │ │ └── JobMapper.java
│ │ │ ├── service/
│ │ │ │ ├── JobService.java
│ │ │ │ └── impl/
│ │ │ │ └── JobServiceImpl.java
│ │ │ └── util/
│ │ │ └── JobUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── mappers/
│ │ │ └── JobMapper.xml
│ │ └── static/
│ │ └── job.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── task/
│ └── quartz/
│ └── SpringBootDemoTaskQuartzApplicationTests.java
├── demo-task-xxl-job/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── xkcoding/
│ │ └── task/
│ │ └── xxl/
│ │ └── job/
│ │ ├── SpringBootDemoTaskXxlJobApplication.java
│ │ ├── config/
│ │ │ ├── XxlJobConfig.java
│ │ │ └── props/
│ │ │ └── XxlJobProps.java
│ │ ├── controller/
│ │ │ └── ManualOperateController.java
│ │ └── task/
│ │ └── DemoTask.java
│ └── resources/
│ └── application.yml
├── demo-template-beetl/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── template/
│ │ │ └── beetl/
│ │ │ ├── SpringBootDemoTemplateBeetlApplication.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── UserController.java
│ │ │ └── model/
│ │ │ └── User.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ ├── common/
│ │ │ └── head.html
│ │ └── page/
│ │ ├── index.btl
│ │ └── login.btl
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── template/
│ └── beetl/
│ └── SpringBootDemoTemplateBeetlApplicationTests.java
├── demo-template-enjoy/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── template/
│ │ │ └── enjoy/
│ │ │ ├── SpringBootDemoTemplateEnjoyApplication.java
│ │ │ ├── config/
│ │ │ │ └── EnjoyConfig.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── UserController.java
│ │ │ └── model/
│ │ │ └── User.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ ├── common/
│ │ │ └── head.html
│ │ └── page/
│ │ ├── index.html
│ │ └── login.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── template/
│ └── enjoy/
│ └── SpringBootDemoTemplateEnjoyApplicationTests.java
├── demo-template-freemarker/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── template/
│ │ │ └── freemarker/
│ │ │ ├── SpringBootDemoTemplateFreemarkerApplication.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── UserController.java
│ │ │ └── model/
│ │ │ └── User.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ ├── common/
│ │ │ └── head.ftl
│ │ └── page/
│ │ ├── index.ftl
│ │ └── login.ftl
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── template/
│ └── freemarker/
│ └── SpringBootDemoTemplateFreemarkerApplicationTests.java
├── demo-template-thymeleaf/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── template/
│ │ │ └── thymeleaf/
│ │ │ ├── SpringBootDemoTemplateThymeleafApplication.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── UserController.java
│ │ │ └── model/
│ │ │ └── User.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ ├── common/
│ │ │ └── head.html
│ │ └── page/
│ │ ├── index.html
│ │ └── login.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── template/
│ └── thymeleaf/
│ └── SpringBootDemoTemplateThymeleafApplicationTests.java
├── demo-tio/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── springbootdemotio/
│ │ │ └── SpringBootDemoTioApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── springbootdemotio/
│ └── SpringBootDemoTioApplicationTests.java
├── demo-uflo/
│ ├── .gitignore
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── uflo/
│ │ │ └── SpringBootDemoUfloApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── uflo/
│ └── SpringBootDemoUfloApplicationTests.java
├── demo-upload/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── upload/
│ │ │ ├── SpringBootDemoUploadApplication.java
│ │ │ ├── config/
│ │ │ │ └── UploadConfig.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── UploadController.java
│ │ │ └── service/
│ │ │ ├── IQiNiuService.java
│ │ │ └── impl/
│ │ │ └── QiNiuServiceImpl.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── templates/
│ │ └── index.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── upload/
│ └── SpringBootDemoUploadApplicationTests.java
├── demo-ureport2/
│ ├── .gitignore
│ ├── README.md
│ ├── doc/
│ │ ├── sql/
│ │ │ └── t_user_ureport2.sql
│ │ └── ureport2/
│ │ └── user_inner_datasource.ureport.xml
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── ureport2/
│ │ │ ├── SpringBootDemoUreport2Application.java
│ │ │ └── config/
│ │ │ └── InnerDatasource.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── ureport2/
│ └── SpringBootDemoUreport2ApplicationTests.java
├── demo-urule/
│ ├── .gitignore
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── urule/
│ │ │ └── SpringBootDemoUruleApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── urule/
│ └── SpringBootDemoUruleApplicationTests.java
├── demo-war/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── war/
│ │ │ └── SpringBootDemoWarApplication.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── war/
│ └── SpringBootDemoWarApplicationTests.java
├── demo-websocket/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── websocket/
│ │ │ ├── SpringBootDemoWebsocketApplication.java
│ │ │ ├── common/
│ │ │ │ └── WebSocketConsts.java
│ │ │ ├── config/
│ │ │ │ └── WebSocketConfig.java
│ │ │ ├── controller/
│ │ │ │ └── ServerController.java
│ │ │ ├── model/
│ │ │ │ ├── Server.java
│ │ │ │ └── server/
│ │ │ │ ├── Cpu.java
│ │ │ │ ├── Jvm.java
│ │ │ │ ├── Mem.java
│ │ │ │ ├── Sys.java
│ │ │ │ └── SysFile.java
│ │ │ ├── payload/
│ │ │ │ ├── KV.java
│ │ │ │ ├── ServerVO.java
│ │ │ │ └── server/
│ │ │ │ ├── CpuVO.java
│ │ │ │ ├── JvmVO.java
│ │ │ │ ├── MemVO.java
│ │ │ │ ├── SysFileVO.java
│ │ │ │ └── SysVO.java
│ │ │ ├── task/
│ │ │ │ └── ServerTask.java
│ │ │ └── util/
│ │ │ ├── IpUtil.java
│ │ │ └── ServerUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── static/
│ │ ├── js/
│ │ │ └── stomp.js
│ │ └── server.html
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── websocket/
│ └── SpringBootDemoWebsocketApplicationTests.java
├── demo-websocket-socketio/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── websocket/
│ │ │ └── socketio/
│ │ │ ├── SpringBootDemoWebsocketSocketioApplication.java
│ │ │ ├── config/
│ │ │ │ ├── DbTemplate.java
│ │ │ │ ├── Event.java
│ │ │ │ ├── ServerConfig.java
│ │ │ │ └── WsConfig.java
│ │ │ ├── controller/
│ │ │ │ └── MessageController.java
│ │ │ ├── handler/
│ │ │ │ └── MessageEventHandler.java
│ │ │ ├── init/
│ │ │ │ └── ServerRunner.java
│ │ │ └── payload/
│ │ │ ├── BroadcastMessageRequest.java
│ │ │ ├── GroupMessageRequest.java
│ │ │ ├── JoinRequest.java
│ │ │ └── SingleMessageRequest.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── static/
│ │ ├── bootstrap.css
│ │ ├── index.html
│ │ └── js/
│ │ └── socket.io/
│ │ └── socket.io.js
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── websocket/
│ └── socketio/
│ └── SpringBootDemoWebsocketSocketioApplicationTests.java
├── demo-zookeeper/
│ ├── .gitignore
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── xkcoding/
│ │ │ └── zookeeper/
│ │ │ ├── SpringBootDemoZookeeperApplication.java
│ │ │ ├── annotation/
│ │ │ │ ├── LockKeyParam.java
│ │ │ │ └── ZooLock.java
│ │ │ ├── aspectj/
│ │ │ │ └── ZooLockAspect.java
│ │ │ └── config/
│ │ │ ├── ZkConfig.java
│ │ │ └── props/
│ │ │ └── ZkProps.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── xkcoding/
│ └── zookeeper/
│ └── SpringBootDemoZookeeperApplicationTests.java
├── jd.md
└── pom.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .codacy.yml
================================================
---
exclude_paths:
- '**.md'
- '**/**.md'
- '**.sql'
- '**.html'
- '**/static/**'
- '**/templates/**'
- '**/test/**'
================================================
FILE: .editorconfig
================================================
# 开发组IDE 编辑器标准
root = true
[*]
indent_size = 2
charset = utf-8
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{groovy, java, kt, kts, xsd}]
indent_size = 4
================================================
FILE: .gitee/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: 报告缺陷
about: 报告缺陷以帮助我们改进
title: "[BUG]"
labels: bug
assignees: xkcoding
---
**请先看[《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md?utm_source=hacpai.com)**,并尝试到 **[issue 列表](https://github.com/xkcoding/spring-boot-demo/issues)** 搜寻是否已经有人遇到过同样的问题。
----
### 描述问题
请尽量清晰精准地描述你碰到的问题。
```bash
日志内容
```
### 期待的结果
请尽量清晰精准地描述你所期待的结果。
### 截屏或录像
如果可能,请尽量附加截图或录像来描述你遇到的问题。
### 其他信息
请提供其他附加信息帮助我们诊断问题。
================================================
FILE: .gitee/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: 请求新功能
about: 提出你期待的功能特性
title: "[FEATURE]"
labels: feature
assignees: xkcoding
---
### 你在什么场景下需要该功能?
请尽量清晰精准地描述你碰到的问题。
### 描述可能的解决方案
请尽量清晰精准地描述你期待我们要做的,描述你想到的实现方案。
### 描述你认为的候选方案
请尽量清晰精准地描述你能接受的候选解决方案。
### 其他信息
请提供关于该功能建议的其他附加信息。
================================================
FILE: .gitee/PULL_REQUEST_TEMPLATE.md
================================================
* PR 修复缺陷请先开 `issue` **[报告缺陷](https://github.com/xkcoding/spring-boot-demo/issues/new?template=bug_report.md)**
* PR 提交新特性请先开 `issue` **[报告新特性](https://github.com/xkcoding/spring-boot-demo/issues/new?template=feature_request.md)**
* PR 请提交到 `dev` 开发分支上
* 我们对编码风格有着较为严格的要求,请在阅读代码后模仿类似风格提交
* 欢迎通过 PR 给我们补充案例
================================================
FILE: .github/FUNDING.yml
================================================
custom: https://docs.xkcoding.com/SPONSER.html
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: 报告缺陷
about: 报告缺陷以帮助我们改进
title: "[BUG]"
labels: bug
assignees: xkcoding
---
**请先看[《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md?utm_source=hacpai.com)**,并尝试到 **[issue 列表](https://github.com/xkcoding/spring-boot-demo/issues)** 搜寻是否已经有人遇到过同样的问题。
----
### 描述问题
请尽量清晰精准地描述你碰到的问题。
```bash
日志内容
```
### 期待的结果
请尽量清晰精准地描述你所期待的结果。
### 截屏或录像
如果可能,请尽量附加截图或录像来描述你遇到的问题。
### 其他信息
请提供其他附加信息帮助我们诊断问题。
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: 请求新功能
about: 提出你期待的功能特性
title: "[FEATURE]"
labels: feature
assignees: xkcoding
---
### 你在什么场景下需要该功能?
请尽量清晰精准地描述你碰到的问题。
### 描述可能的解决方案
请尽量清晰精准地描述你期待我们要做的,描述你想到的实现方案。
### 描述你认为的候选方案
请尽量清晰精准地描述你能接受的候选解决方案。
### 其他信息
请提供关于该功能建议的其他附加信息。
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
* PR 修复缺陷请先开 `issue` **[报告缺陷](https://github.com/xkcoding/spring-boot-demo/issues/new?template=bug_report.md)**
* PR 提交新特性请先开 `issue` **[报告新特性](https://github.com/xkcoding/spring-boot-demo/issues/new?template=feature_request.md)**
* PR 请提交到 `dev` 开发分支上
* 我们对编码风格有着较为严格的要求,请在阅读代码后模仿类似风格提交
* 欢迎通过 PR 给我们补充案例
================================================
FILE: .github/workflows/maven.yml
================================================
name: GitHub CI
on:
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Maven
run: mvn clean package -DskipTests=true -Dmaven.javadoc.skip=true -B -V
================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### xkcoding-后端 template
### Spring Boot ###
target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
gen/
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### LOGS ###
logs/
*.log
### Mac OS ###
.DS_Store
### VS CODE ###
.vscode/
================================================
FILE: .travis.yml
================================================
# 语言
language: java
# 执行脚本
script: "mvn clean package -DskipTests=true -Dmaven.javadoc.skip=true -B -V"
# 通知
notifications:
email:
recipients:
- 237497819@qq.com
on_success: always # default: change
on_failure: always # default: always
# 缓存
cache:
directories:
- '$HOME/.m2/repository'
branches:
only:
- master
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Yangkai.Shen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.en.md
================================================
English | 中文
## Introduction
`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`),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.
## 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.
- branch v-1.5.x: Based on Spring Boot version `1.5.8.RELEASE`. Every module's parent dependency is spring-boot-demo-parent. But since the feedback shows that it is not much friendly to many new learners, this branch will not be mantained any more. All of the demos will be moved to branch master. Everyone could still study at this branch but it's suggested to study at branch master while Spring Boot has much new content over version `2.x`.
## Environment
- **JDK 1.8 +**
- **Maven 3.5 +**
- **IntelliJ IDEA ULTIMATE 2018.2 +** (*Note: Please use IDEA and make sure plugin `lombok` installed.*)
- **Mysql 5.7 +** (*Please use version 5.7 or higher because mysql has some new features and is not backward compatible at version 5.7. Althought this project will try to avoid this incompatibility*)
## Getting Started
> Note: If you has been forked this project, need to sync the project's code, please see: https://xkcoding.com/2018/09/18/how-to-update-the-fork-project.html
1. `git clone https://github.com/xkcoding/spring-boot-demo.git`
2. Open the cloned project in IDEA
3. Import the `pom.xml` file from the root directory using `Maven Projects` panel
4. If you can not find `Maven Projects` panel, try to tick `View -> Tool Buttons` on and the `Maven Projects` panel will appear on the right side of IDEA.
5. Find each Application class to run each module.
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~`**
## Stargazers over time
[](https://starchart.cc/xkcoding/spring-boot-demo)
## Appendix
### Recommended Open source
- `JustAuth`:The most comprehensive open source library for third-party logins in history,https://github.com/justauth/JustAuth
- `Mica`:Spring Boot microservices efficient development toolset,https://github.com/lets-mica/mica
- `awesome-collector`:https://github.com/P-P-X/awesome-collector
- `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
### TODO
View the [TODO](./TODO.en.md) file
### Introduction of each Module
| 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
================================================
FILE: README.md
================================================
中文 | English
## 项目简介
`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(`数据库初始化`)、UReport2(`中国式复杂报表`)。
> 如果大家还有想要集成的demo,也可在 [issue](https://github.com/xkcoding/spring-boot-demo/issues/new) 里提需求。我会额外添加在 [TODO](./TODO.md) 列表里。✊
## 分支介绍
- master 分支:基于 Spring Boot 版本 `2.1.0.RELEASE`,每个 Module 的 parent 依赖根目录下的 pom.xml,主要用于管理每个 Module 的通用依赖版本,方便大家学习。
- v-1.5.x 分支:基于 Spring Boot 版本 `1.5.8.RELEASE`,每个 Module 均依赖 spring-boot-demo-parent,有挺多同学们反映这种方式对新手不是很友好,运行起来有些难度,因此 ***此分支(v-1.5.x)会停止开发维护*** ,所有内容会慢慢以 master 分支的形式同步过去,此分支暂未完成的,也会直接在 master 分支上加,在此分支学习的同学们,仍然可以在此分支学习,但是建议后期切换到master分支,会更加容易,毕竟官方已经将 Spring Boot 升级到 2.x 版本。🙂
## 开发环境
- **JDK 1.8 +**
- **Maven 3.5 +**
- **IntelliJ IDEA ULTIMATE 2018.2 +** (*注意:务必使用 IDEA 开发,同时保证安装 `lombok` 插件*)
- **Mysql 5.7 +** (*尽量保证使用 5.7 版本以上,因为 5.7 版本加了一些新特性,同时不向下兼容。本 demo 里会尽量避免这种不兼容的地方,但还是建议尽量保证 5.7 版本以上*)
## 运行方式
> 提示:如果是 fork 的朋友,同步代码的请参考:https://xkcoding.com/2018/09/18/how-to-update-the-fork-project.html
1. `git clone https://github.com/xkcoding/spring-boot-demo.git`
2. 使用 IDEA 打开 clone 下来的项目
3. 在 IDEA 中 Maven Projects 的面板导入项目根目录下 的 `pom.xml` 文件
4. Maven Projects 找不到的童鞋,可以勾上 IDEA 顶部工具栏的 View -> Tool Buttons ,然后 Maven Projects 的面板就会出现在 IDEA 的右侧
5. 找到各个 Module 的 Application 类就可以运行各个 demo 了
6. **`注意:每个 demo 均有详细的 README 配套,食用 demo 前记得先看看哦~`**
7. **`注意:运行各个 demo 之前,有些是需要事先初始化数据库数据的,亲们别忘记了哦~`**
## 项目趋势
[](https://starchart.cc/xkcoding/spring-boot-demo)
## 其他
### 团队纳新
组内招人啦,HC 巨多,Base 杭州,感兴趣的小伙伴,查看 [岗位详情](./jd.md)
### 开源推荐

- `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
### 开发计划
查看 [TODO](./TODO.md) 文件
### 各 Module 介绍
| 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,实现中国式复杂报表设计 |
### 特别感谢
- 感谢 [七牛云](https://portal.qiniu.com/signup?utm_source=kaiyuan&utm_media=xkcoding) 提供的免费云存储与 CDN 加速支持
- 感谢史上最牛的代码生成插件 [MyBatisCodeHelper-Pro](https://gejun123456.github.io/MyBatisCodeHelper-Pro/#/?id=mybatiscodehelper-pro) 提供的永久激活码
- **感谢 JetBrains 提供的免费开源 License**
### License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2018 Yangkai.Shen
================================================
FILE: TODO.en.md
================================================
# spring-boot-demo Project TODO List
## Module plan (completed: 55 / 66)
- [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
Try to ensure that the corresponding demos are integrated in the order above.
================================================
FILE: TODO.md
================================================
# spring-boot-demo 项目待办列表
## 模块计划(已完成:55 / 66)
- [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,项目启动时初始化数据库表结构,同时支持数据库脚本版本控制)~~
## 备注
尽量保证按照上面的顺序集成相应的 demo。
================================================
FILE: demo-activiti/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-activiti/pom.xml
================================================
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
================================================
FILE: demo-activiti/src/main/java/com/xkcoding/activiti/SpringBootDemoActivitiApplication.java
================================================
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);
}
}
================================================
FILE: demo-activiti/src/main/java/com/xkcoding/activiti/config/SecurityConfiguration.java
================================================
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();
}
}
================================================
FILE: demo-activiti/src/main/java/com/xkcoding/activiti/util/SecurityUtil.java
================================================
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);
}
}
================================================
FILE: demo-activiti/src/main/resources/application.yml
================================================
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring-boot-demo
username: root
password: root
hikari:
data-source-properties:
useSSL: false
serverTimezone: GMT+8
useUnicode: true
characterEncoding: utf8
# 这个必须要加,否则 Activiti 自动建表会失败
nullCatalogMeansCurrent: true
activiti:
history-level: full
db-history-used: true
================================================
FILE: demo-activiti/src/main/resources/processes/team01.bpmn
================================================
_6
_6
_7
_7
_8
_8
================================================
FILE: demo-activiti/src/test/java/com/xkcoding/activiti/SpringBootDemoActivitiApplicationTests.java
================================================
package com.xkcoding.activiti;
import com.xkcoding.activiti.util.SecurityUtil;
import org.activiti.api.process.model.ProcessDefinition;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
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;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootDemoActivitiApplicationTests {
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private SecurityUtil securityUtil;
@Test
public void contextLoads() {
securityUtil.logInAs("salaboy");
Page processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10));
processDefinitionPage.getContent().forEach(System.out::println);
}
}
================================================
FILE: demo-actuator/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-actuator/README.md
================================================
# spring-boot-demo-actuator
> 本 demo 主要演示了如何在 Spring Boot 中通过 actuator 检查项目运行情况
## pom.xml
```xml
4.0.0
spring-boot-demo-actuator
1.0.0-SNAPSHOT
jar
spring-boot-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
spring-boot-demo-actuator
org.springframework.boot
spring-boot-maven-plugin
```
## application.yml
```yaml
server:
port: 8080
servlet:
context-path: /demo
# 若要访问端点信息,需要配置用户名和密码
spring:
security:
user:
name: xkcoding
password: 123456
management:
# 端点信息接口使用的端口,为了和主系统接口使用的端口进行分离
server:
port: 8090
servlet:
context-path: /sys
# 端点健康情况,默认值"never",设置为"always"可以显示硬盘使用情况和线程情况
endpoint:
health:
show-details: always
# 设置端点暴露的哪些内容,默认["health","info"],设置"*"代表暴露所有可访问的端点
endpoints:
web:
exposure:
include: '*'
```
## 端点暴露地址
将项目运行起来之后,会在**控制台**里查看所有可以访问的端口信息
1. 打开浏览器,访问:http://localhost:8090/sys/actuator/mappings ,输入用户名(xkcoding)密码(123456)即可看到所有的mapping信息
2. 访问:http://localhost:8090/sys/actuator/beans ,输入用户名(xkcoding)密码(123456)即可看到所有 Spring 管理的Bean
3. 其余可访问的路径,参见文档
## 参考
- actuator文档:https://docs.spring.io/spring-boot/docs/2.0.5.RELEASE/reference/htmlsingle/#production-ready
- 具体可以访问哪些路径,参考: https://docs.spring.io/spring-boot/docs/2.0.5.RELEASE/reference/htmlsingle/#production-ready-endpoints
================================================
FILE: demo-actuator/pom.xml
================================================
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
================================================
FILE: demo-actuator/src/main/java/com/xkcoding/actuator/SpringBootDemoActuatorApplication.java
================================================
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);
}
}
================================================
FILE: demo-actuator/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
# 若要访问端点信息,需要配置用户名和密码
spring:
security:
user:
name: xkcoding
password: 123456
management:
# 端点信息接口使用的端口,为了和主系统接口使用的端口进行分离
server:
port: 8090
servlet:
context-path: /sys
# 端点健康情况,默认值"never",设置为"always"可以显示硬盘使用情况和线程情况
endpoint:
health:
show-details: always
# 设置端点暴露的哪些内容,默认["health","info"],设置"*"代表暴露所有可访问的端点
endpoints:
web:
exposure:
include: '*'
================================================
FILE: demo-actuator/src/test/java/com/xkcoding/actuator/SpringBootDemoActuatorApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-admin/README.md
================================================
# spring-boot-demo-admin
> 本 demo 主要演示了 Spring Boot 如何集成 Admin 管控台,监控管理 Spring Boot 应用,分别为 admin 服务端和 admin 客户端,两个模块。
## 运行步骤
1. 进入 `spring-boot-demo-admin-server` 服务端,启动管控台服务端程序
2. 进入 `spring-boot-demo-admin-client` 客户端,启动客户端程序,注册到服务端
3. 观察服务端里,客户端程序的运行状态等信息
## pom.xml
```xml
spring-boot-demo
com.xkcoding
1.0.0-SNAPSHOT
4.0.0
spring-boot-demo-admin
pom
2.1.0
spring-boot-demo-admin-client
spring-boot-demo-admin-server
de.codecentric
spring-boot-admin-dependencies
${spring-boot-admin.version}
pom
import
```
================================================
FILE: demo-admin/admin-client/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-admin/admin-client/README.md
================================================
# spring-boot-demo-admin-client
> 本 demo 主要演示了普通项目如何集成 Spring Boot Admin,并把自己的运行状态交给 Spring Boot Admin 进行展现。
## pom.xml
```xml
4.0.0
spring-boot-demo-admin-client
1.0.0-SNAPSHOT
jar
spring-boot-demo-admin-client
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-client
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-test
test
spring-boot-demo-admin-client
org.springframework.boot
spring-boot-maven-plugin
```
## application.yml
```yaml
server:
port: 8080
servlet:
context-path: /demo
spring:
application:
# Spring Boot Admin展示的客户端项目名,不设置,会使用自动生成的随机id
name: spring-boot-demo-admin-client
boot:
admin:
client:
# Spring Boot Admin 服务端地址
url: "http://localhost:8000/"
instance:
metadata:
# 客户端端点信息的安全认证信息
user.name: ${spring.security.user.name}
user.password: ${spring.security.user.password}
security:
user:
name: xkcoding
password: 123456
management:
endpoint:
health:
# 端点健康情况,默认值"never",设置为"always"可以显示硬盘使用情况和线程情况
show-details: always
endpoints:
web:
exposure:
# 设置端点暴露的哪些内容,默认["health","info"],设置"*"代表暴露所有可访问的端点
include: "*"
```
================================================
FILE: demo-admin/admin-client/pom.xml
================================================
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
================================================
FILE: demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplication.java
================================================
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);
}
}
================================================
FILE: demo-admin/admin-client/src/main/java/com/xkcoding/admin/client/controller/IndexController.java
================================================
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.";
}
}
================================================
FILE: demo-admin/admin-client/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
spring:
application:
# Spring Boot Admin展示的客户端项目名,不设置,会使用自动生成的随机id
name: spring-boot-demo-admin-client
boot:
admin:
client:
# Spring Boot Admin 服务端地址
url: "http://localhost:8000/"
instance:
metadata:
# 客户端端点信息的安全认证信息
user.name: ${spring.security.user.name}
user.password: ${spring.security.user.password}
security:
user:
name: xkcoding
password: 123456
management:
endpoint:
health:
# 端点健康情况,默认值"never",设置为"always"可以显示硬盘使用情况和线程情况
show-details: always
endpoints:
web:
exposure:
# 设置端点暴露的哪些内容,默认["health","info"],设置"*"代表暴露所有可访问的端点
include: "*"
================================================
FILE: demo-admin/admin-client/src/test/java/com/xkcoding/admin/client/SpringBootDemoAdminClientApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-admin/admin-server/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-admin/admin-server/README.md
================================================
# 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
```
================================================
FILE: demo-admin/admin-server/pom.xml
================================================
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
================================================
FILE: demo-admin/admin-server/src/main/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplication.java
================================================
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);
}
}
================================================
FILE: demo-admin/admin-server/src/main/resources/application.yml
================================================
server:
port: 8000
================================================
FILE: demo-admin/admin-server/src/test/java/com/xkcoding/admin/server/SpringBootDemoAdminServerApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-admin/pom.xml
================================================
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
================================================
FILE: demo-async/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-async/README.md
================================================
# 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
================================================
FILE: demo-async/pom.xml
================================================
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
================================================
FILE: demo-async/src/main/java/com/xkcoding/async/SpringBootDemoAsyncApplication.java
================================================
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);
}
}
================================================
FILE: demo-async/src/main/java/com/xkcoding/async/task/TaskFactory.java
================================================
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());
}
}
================================================
FILE: demo-async/src/main/resources/application.yml
================================================
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-
================================================
FILE: demo-async/src/test/java/com/xkcoding/async/SpringBootDemoAsyncApplicationTests.java
================================================
package com.xkcoding.async;
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 SpringBootDemoAsyncApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-async/src/test/java/com/xkcoding/async/task/TaskFactoryTest.java
================================================
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));
}
}
================================================
FILE: demo-cache-ehcache/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-cache-ehcache/README.md
================================================
# 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
================================================
FILE: demo-cache-ehcache/pom.xml
================================================
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
================================================
FILE: demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplication.java
================================================
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);
}
}
================================================
FILE: demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/entity/User.java
================================================
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;
}
================================================
FILE: demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/UserService.java
================================================
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);
}
================================================
FILE: demo-cache-ehcache/src/main/java/com/xkcoding/cache/ehcache/service/impl/UserServiceImpl.java
================================================
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);
}
}
================================================
FILE: demo-cache-ehcache/src/main/resources/application.yml
================================================
spring:
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
logging:
level:
com.xkcoding: debug
================================================
FILE: demo-cache-ehcache/src/main/resources/ehcache.xml
================================================
================================================
FILE: demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/SpringBootDemoCacheEhcacheApplicationTests.java
================================================
package com.xkcoding.cache.ehcache;
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 SpringBootDemoCacheEhcacheApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-cache-ehcache/src/test/java/com/xkcoding/cache/ehcache/service/UserServiceTest.java
================================================
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);
}
}
================================================
FILE: demo-cache-redis/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-cache-redis/README.md
================================================
# 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
================================================
FILE: demo-cache-redis/pom.xml
================================================
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
================================================
FILE: demo-cache-redis/src/main/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplication.java
================================================
package com.xkcoding.cache.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootDemoCacheRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoCacheRedisApplication.class, args);
}
}
================================================
FILE: demo-cache-redis/src/main/java/com/xkcoding/cache/redis/config/RedisConfig.java
================================================
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();
}
}
================================================
FILE: demo-cache-redis/src/main/java/com/xkcoding/cache/redis/entity/User.java
================================================
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;
}
================================================
FILE: demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/UserService.java
================================================
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);
}
================================================
FILE: demo-cache-redis/src/main/java/com/xkcoding/cache/redis/service/impl/UserServiceImpl.java
================================================
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);
}
}
================================================
FILE: demo-cache-redis/src/main/resources/application.yml
================================================
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
================================================
FILE: demo-cache-redis/src/test/java/com/xkcoding/cache/redis/RedisTest.java
================================================
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);
}
}
================================================
FILE: demo-cache-redis/src/test/java/com/xkcoding/cache/redis/SpringBootDemoCacheRedisApplicationTests.java
================================================
package com.xkcoding.cache.redis;
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 SpringBootDemoCacheRedisApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-cache-redis/src/test/java/com/xkcoding/cache/redis/service/UserServiceTest.java
================================================
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);
}
}
================================================
FILE: demo-codegen/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
================================================
FILE: demo-codegen/README.md
================================================
# 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)
================================================
FILE: demo-codegen/pom.xml
================================================
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
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/SpringBootDemoCodegenApplication.java
================================================
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);
}
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/common/IResultCode.java
================================================
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();
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/common/PageResult.java
================================================
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;
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/common/R.java
================================================
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);
}
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/common/ResultCode.java
================================================
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;
}
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/constants/GenConstants.java
================================================
package com.xkcoding.codegen.constants;
/**
*
* 常量池
*
*
* @author yangkai.shen
* @date Created in 2019-03-22 10:04
*/
public interface GenConstants {
/**
* 签名
*/
String SIGNATURE = "xkcoding代码生成";
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/controller/CodeGenController.java
================================================
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);
}
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/entity/ColumnEntity.java
================================================
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;
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/entity/GenConfig.java
================================================
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;
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableEntity.java
================================================
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;
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/entity/TableRequest.java
================================================
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;
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/service/CodeGenService.java
================================================
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);
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/service/impl/CodeGenServiceImpl.java
================================================
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;
}
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/utils/CodeGenUtil.java
================================================
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;
}
}
================================================
FILE: demo-codegen/src/main/java/com/xkcoding/codegen/utils/DbUtil.java
================================================
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;
}
}
================================================
FILE: demo-codegen/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
================================================
FILE: demo-codegen/src/main/resources/generator.properties
================================================
#\u4EE3\u7801\u751F\u6210\u5668\uFF0C\u914D\u7F6E\u4FE1\u606F
mainPath=com.xkcoding
#\u5305\u540D
package=com.xkcoding
moduleName=generator
#\u4F5C\u8005
author=Yangkai.Shen
#\u8868\u524D\u7F00(\u7C7B\u540D\u4E0D\u4F1A\u5305\u542B\u8868\u524D\u7F00)
tablePrefix=tb_
#\u7C7B\u578B\u8F6C\u6362\uFF0C\u914D\u7F6E\u4FE1\u606F
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
================================================
FILE: demo-codegen/src/main/resources/jdbc_type.properties
================================================
tinyint=TINYINT
smallint=SMALLINT
mediumint=MEDIUMINT
int=INTEGER
integer=INTEGER
bigint=BIGINT
float=FLOAT
double=DOUBLE
decimal=DECIMAL
bit=BIT
char=CHAR
varchar=VARCHAR
tinytext=VARCHAR
text=VARCHAR
mediumtext=VARCHAR
longtext=VARCHAR
date=DATE
datetime=DATETIME
timestamp=TIMESTAMP
blob=BLOB
longblob=LONGBLOB
================================================
FILE: demo-codegen/src/main/resources/logback-spring.xml
================================================
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
================================================
FILE: demo-codegen/src/main/resources/static/index.html
================================================
代码生成器
{{mysqlPrepend}}
{{oraclePrepend}}
{{mssqlPrepend}}
查询
{{ row.tableName }}
生成代码
生成配置
取消
生成代码
================================================
FILE: demo-codegen/src/main/resources/static/libs/datejs/date-zh-CN.js
================================================
/**
* @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;ispan:last-child{font-weight:700;color:#515a6e}.ivu-breadcrumb>span:last-child .ivu-breadcrumb-item-separator{display:none}.ivu-breadcrumb-item-separator{margin:0 8px;color:#dcdee2}.ivu-breadcrumb-item-link>.ivu-icon+span{margin-left:4px}/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto;resize:vertical}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-tap-highlight-color:transparent}:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box}body{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;font-size:12px;line-height:1.5;color:#515a6e;background-color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article,aside,blockquote,body,button,dd,details,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,input,legend,li,menu,nav,ol,p,section,td,textarea,th,ul{margin:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}input::-ms-clear,input::-ms-reveal{display:none}a{color:#2d8cf0;background:0 0;text-decoration:none;outline:0;cursor:pointer;-webkit-transition:color .2s ease;transition:color .2s ease}a:hover{color:#57a3f3}a:active{color:#2b85e4}a:active,a:hover{outline:0;text-decoration:none}a[disabled]{color:#ccc;cursor:not-allowed;pointer-events:none}code,kbd,pre,samp{font-family:Consolas,Menlo,Courier,monospace}@font-face{font-family:Ionicons;src:url(fonts/ionicons.ttf?v=3.0.0) format("truetype"),url(fonts/ionicons.woff?v=3.0.0) format("woff"),url(fonts/ionicons.svg?v=3.0.0#Ionicons) format("svg");font-weight:400;font-style:normal}.ivu-icon{display:inline-block;font-family:Ionicons;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;vertical-align:middle}.ivu-icon-ios-add-circle-outline:before{content:"\f100"}.ivu-icon-ios-add-circle:before{content:"\f101"}.ivu-icon-ios-add:before{content:"\f102"}.ivu-icon-ios-alarm-outline:before{content:"\f103"}.ivu-icon-ios-alarm:before{content:"\f104"}.ivu-icon-ios-albums-outline:before{content:"\f105"}.ivu-icon-ios-albums:before{content:"\f106"}.ivu-icon-ios-alert-outline:before{content:"\f107"}.ivu-icon-ios-alert:before{content:"\f108"}.ivu-icon-ios-american-football-outline:before{content:"\f109"}.ivu-icon-ios-american-football:before{content:"\f10a"}.ivu-icon-ios-analytics-outline:before{content:"\f10b"}.ivu-icon-ios-analytics:before{content:"\f10c"}.ivu-icon-ios-aperture-outline:before{content:"\f10d"}.ivu-icon-ios-aperture:before{content:"\f10e"}.ivu-icon-ios-apps-outline:before{content:"\f10f"}.ivu-icon-ios-apps:before{content:"\f110"}.ivu-icon-ios-appstore-outline:before{content:"\f111"}.ivu-icon-ios-appstore:before{content:"\f112"}.ivu-icon-ios-archive-outline:before{content:"\f113"}.ivu-icon-ios-archive:before{content:"\f114"}.ivu-icon-ios-arrow-back:before{content:"\f115"}.ivu-icon-ios-arrow-down:before{content:"\f116"}.ivu-icon-ios-arrow-dropdown-circle:before{content:"\f117"}.ivu-icon-ios-arrow-dropdown:before{content:"\f118"}.ivu-icon-ios-arrow-dropleft-circle:before{content:"\f119"}.ivu-icon-ios-arrow-dropleft:before{content:"\f11a"}.ivu-icon-ios-arrow-dropright-circle:before{content:"\f11b"}.ivu-icon-ios-arrow-dropright:before{content:"\f11c"}.ivu-icon-ios-arrow-dropup-circle:before{content:"\f11d"}.ivu-icon-ios-arrow-dropup:before{content:"\f11e"}.ivu-icon-ios-arrow-forward:before{content:"\f11f"}.ivu-icon-ios-arrow-round-back:before{content:"\f120"}.ivu-icon-ios-arrow-round-down:before{content:"\f121"}.ivu-icon-ios-arrow-round-forward:before{content:"\f122"}.ivu-icon-ios-arrow-round-up:before{content:"\f123"}.ivu-icon-ios-arrow-up:before{content:"\f124"}.ivu-icon-ios-at-outline:before{content:"\f125"}.ivu-icon-ios-at:before{content:"\f126"}.ivu-icon-ios-attach:before{content:"\f127"}.ivu-icon-ios-backspace-outline:before{content:"\f128"}.ivu-icon-ios-backspace:before{content:"\f129"}.ivu-icon-ios-barcode-outline:before{content:"\f12a"}.ivu-icon-ios-barcode:before{content:"\f12b"}.ivu-icon-ios-baseball-outline:before{content:"\f12c"}.ivu-icon-ios-baseball:before{content:"\f12d"}.ivu-icon-ios-basket-outline:before{content:"\f12e"}.ivu-icon-ios-basket:before{content:"\f12f"}.ivu-icon-ios-basketball-outline:before{content:"\f130"}.ivu-icon-ios-basketball:before{content:"\f131"}.ivu-icon-ios-battery-charging:before{content:"\f132"}.ivu-icon-ios-battery-dead:before{content:"\f133"}.ivu-icon-ios-battery-full:before{content:"\f134"}.ivu-icon-ios-beaker-outline:before{content:"\f135"}.ivu-icon-ios-beaker:before{content:"\f136"}.ivu-icon-ios-beer-outline:before{content:"\f137"}.ivu-icon-ios-beer:before{content:"\f138"}.ivu-icon-ios-bicycle:before{content:"\f139"}.ivu-icon-ios-bluetooth:before{content:"\f13a"}.ivu-icon-ios-boat-outline:before{content:"\f13b"}.ivu-icon-ios-boat:before{content:"\f13c"}.ivu-icon-ios-body-outline:before{content:"\f13d"}.ivu-icon-ios-body:before{content:"\f13e"}.ivu-icon-ios-bonfire-outline:before{content:"\f13f"}.ivu-icon-ios-bonfire:before{content:"\f140"}.ivu-icon-ios-book-outline:before{content:"\f141"}.ivu-icon-ios-book:before{content:"\f142"}.ivu-icon-ios-bookmark-outline:before{content:"\f143"}.ivu-icon-ios-bookmark:before{content:"\f144"}.ivu-icon-ios-bookmarks-outline:before{content:"\f145"}.ivu-icon-ios-bookmarks:before{content:"\f146"}.ivu-icon-ios-bowtie-outline:before{content:"\f147"}.ivu-icon-ios-bowtie:before{content:"\f148"}.ivu-icon-ios-briefcase-outline:before{content:"\f149"}.ivu-icon-ios-briefcase:before{content:"\f14a"}.ivu-icon-ios-browsers-outline:before{content:"\f14b"}.ivu-icon-ios-browsers:before{content:"\f14c"}.ivu-icon-ios-brush-outline:before{content:"\f14d"}.ivu-icon-ios-brush:before{content:"\f14e"}.ivu-icon-ios-bug-outline:before{content:"\f14f"}.ivu-icon-ios-bug:before{content:"\f150"}.ivu-icon-ios-build-outline:before{content:"\f151"}.ivu-icon-ios-build:before{content:"\f152"}.ivu-icon-ios-bulb-outline:before{content:"\f153"}.ivu-icon-ios-bulb:before{content:"\f154"}.ivu-icon-ios-bus-outline:before{content:"\f155"}.ivu-icon-ios-bus:before{content:"\f156"}.ivu-icon-ios-cafe-outline:before{content:"\f157"}.ivu-icon-ios-cafe:before{content:"\f158"}.ivu-icon-ios-calculator-outline:before{content:"\f159"}.ivu-icon-ios-calculator:before{content:"\f15a"}.ivu-icon-ios-calendar-outline:before{content:"\f15b"}.ivu-icon-ios-calendar:before{content:"\f15c"}.ivu-icon-ios-call-outline:before{content:"\f15d"}.ivu-icon-ios-call:before{content:"\f15e"}.ivu-icon-ios-camera-outline:before{content:"\f15f"}.ivu-icon-ios-camera:before{content:"\f160"}.ivu-icon-ios-car-outline:before{content:"\f161"}.ivu-icon-ios-car:before{content:"\f162"}.ivu-icon-ios-card-outline:before{content:"\f163"}.ivu-icon-ios-card:before{content:"\f164"}.ivu-icon-ios-cart-outline:before{content:"\f165"}.ivu-icon-ios-cart:before{content:"\f166"}.ivu-icon-ios-cash-outline:before{content:"\f167"}.ivu-icon-ios-cash:before{content:"\f168"}.ivu-icon-ios-chatboxes-outline:before{content:"\f169"}.ivu-icon-ios-chatboxes:before{content:"\f16a"}.ivu-icon-ios-chatbubbles-outline:before{content:"\f16b"}.ivu-icon-ios-chatbubbles:before{content:"\f16c"}.ivu-icon-ios-checkbox-outline:before{content:"\f16d"}.ivu-icon-ios-checkbox:before{content:"\f16e"}.ivu-icon-ios-checkmark-circle-outline:before{content:"\f16f"}.ivu-icon-ios-checkmark-circle:before{content:"\f170"}.ivu-icon-ios-checkmark:before{content:"\f171"}.ivu-icon-ios-clipboard-outline:before{content:"\f172"}.ivu-icon-ios-clipboard:before{content:"\f173"}.ivu-icon-ios-clock-outline:before{content:"\f174"}.ivu-icon-ios-clock:before{content:"\f175"}.ivu-icon-ios-close-circle-outline:before{content:"\f176"}.ivu-icon-ios-close-circle:before{content:"\f177"}.ivu-icon-ios-close:before{content:"\f178"}.ivu-icon-ios-closed-captioning-outline:before{content:"\f179"}.ivu-icon-ios-closed-captioning:before{content:"\f17a"}.ivu-icon-ios-cloud-circle-outline:before{content:"\f17b"}.ivu-icon-ios-cloud-circle:before{content:"\f17c"}.ivu-icon-ios-cloud-done-outline:before{content:"\f17d"}.ivu-icon-ios-cloud-done:before{content:"\f17e"}.ivu-icon-ios-cloud-download-outline:before{content:"\f17f"}.ivu-icon-ios-cloud-download:before{content:"\f180"}.ivu-icon-ios-cloud-outline:before{content:"\f181"}.ivu-icon-ios-cloud-upload-outline:before{content:"\f182"}.ivu-icon-ios-cloud-upload:before{content:"\f183"}.ivu-icon-ios-cloud:before{content:"\f184"}.ivu-icon-ios-cloudy-night-outline:before{content:"\f185"}.ivu-icon-ios-cloudy-night:before{content:"\f186"}.ivu-icon-ios-cloudy-outline:before{content:"\f187"}.ivu-icon-ios-cloudy:before{content:"\f188"}.ivu-icon-ios-code-download:before{content:"\f189"}.ivu-icon-ios-code-working:before{content:"\f18a"}.ivu-icon-ios-code:before{content:"\f18b"}.ivu-icon-ios-cog-outline:before{content:"\f18c"}.ivu-icon-ios-cog:before{content:"\f18d"}.ivu-icon-ios-color-fill-outline:before{content:"\f18e"}.ivu-icon-ios-color-fill:before{content:"\f18f"}.ivu-icon-ios-color-filter-outline:before{content:"\f190"}.ivu-icon-ios-color-filter:before{content:"\f191"}.ivu-icon-ios-color-palette-outline:before{content:"\f192"}.ivu-icon-ios-color-palette:before{content:"\f193"}.ivu-icon-ios-color-wand-outline:before{content:"\f194"}.ivu-icon-ios-color-wand:before{content:"\f195"}.ivu-icon-ios-compass-outline:before{content:"\f196"}.ivu-icon-ios-compass:before{content:"\f197"}.ivu-icon-ios-construct-outline:before{content:"\f198"}.ivu-icon-ios-construct:before{content:"\f199"}.ivu-icon-ios-contact-outline:before{content:"\f19a"}.ivu-icon-ios-contact:before{content:"\f19b"}.ivu-icon-ios-contacts-outline:before{content:"\f19c"}.ivu-icon-ios-contacts:before{content:"\f19d"}.ivu-icon-ios-contract:before{content:"\f19e"}.ivu-icon-ios-contrast:before{content:"\f19f"}.ivu-icon-ios-copy-outline:before{content:"\f1a0"}.ivu-icon-ios-copy:before{content:"\f1a1"}.ivu-icon-ios-create-outline:before{content:"\f1a2"}.ivu-icon-ios-create:before{content:"\f1a3"}.ivu-icon-ios-crop-outline:before{content:"\f1a4"}.ivu-icon-ios-crop:before{content:"\f1a5"}.ivu-icon-ios-cube-outline:before{content:"\f1a6"}.ivu-icon-ios-cube:before{content:"\f1a7"}.ivu-icon-ios-cut-outline:before{content:"\f1a8"}.ivu-icon-ios-cut:before{content:"\f1a9"}.ivu-icon-ios-desktop-outline:before{content:"\f1aa"}.ivu-icon-ios-desktop:before{content:"\f1ab"}.ivu-icon-ios-disc-outline:before{content:"\f1ac"}.ivu-icon-ios-disc:before{content:"\f1ad"}.ivu-icon-ios-document-outline:before{content:"\f1ae"}.ivu-icon-ios-document:before{content:"\f1af"}.ivu-icon-ios-done-all:before{content:"\f1b0"}.ivu-icon-ios-download-outline:before{content:"\f1b1"}.ivu-icon-ios-download:before{content:"\f1b2"}.ivu-icon-ios-easel-outline:before{content:"\f1b3"}.ivu-icon-ios-easel:before{content:"\f1b4"}.ivu-icon-ios-egg-outline:before{content:"\f1b5"}.ivu-icon-ios-egg:before{content:"\f1b6"}.ivu-icon-ios-exit-outline:before{content:"\f1b7"}.ivu-icon-ios-exit:before{content:"\f1b8"}.ivu-icon-ios-expand:before{content:"\f1b9"}.ivu-icon-ios-eye-off-outline:before{content:"\f1ba"}.ivu-icon-ios-eye-off:before{content:"\f1bb"}.ivu-icon-ios-eye-outline:before{content:"\f1bc"}.ivu-icon-ios-eye:before{content:"\f1bd"}.ivu-icon-ios-fastforward-outline:before{content:"\f1be"}.ivu-icon-ios-fastforward:before{content:"\f1bf"}.ivu-icon-ios-female:before{content:"\f1c0"}.ivu-icon-ios-filing-outline:before{content:"\f1c1"}.ivu-icon-ios-filing:before{content:"\f1c2"}.ivu-icon-ios-film-outline:before{content:"\f1c3"}.ivu-icon-ios-film:before{content:"\f1c4"}.ivu-icon-ios-finger-print:before{content:"\f1c5"}.ivu-icon-ios-flag-outline:before{content:"\f1c6"}.ivu-icon-ios-flag:before{content:"\f1c7"}.ivu-icon-ios-flame-outline:before{content:"\f1c8"}.ivu-icon-ios-flame:before{content:"\f1c9"}.ivu-icon-ios-flash-outline:before{content:"\f1ca"}.ivu-icon-ios-flash:before{content:"\f1cb"}.ivu-icon-ios-flask-outline:before{content:"\f1cc"}.ivu-icon-ios-flask:before{content:"\f1cd"}.ivu-icon-ios-flower-outline:before{content:"\f1ce"}.ivu-icon-ios-flower:before{content:"\f1cf"}.ivu-icon-ios-folder-open-outline:before{content:"\f1d0"}.ivu-icon-ios-folder-open:before{content:"\f1d1"}.ivu-icon-ios-folder-outline:before{content:"\f1d2"}.ivu-icon-ios-folder:before{content:"\f1d3"}.ivu-icon-ios-football-outline:before{content:"\f1d4"}.ivu-icon-ios-football:before{content:"\f1d5"}.ivu-icon-ios-funnel-outline:before{content:"\f1d6"}.ivu-icon-ios-funnel:before{content:"\f1d7"}.ivu-icon-ios-game-controller-a-outline:before{content:"\f1d8"}.ivu-icon-ios-game-controller-a:before{content:"\f1d9"}.ivu-icon-ios-game-controller-b-outline:before{content:"\f1da"}.ivu-icon-ios-game-controller-b:before{content:"\f1db"}.ivu-icon-ios-git-branch:before{content:"\f1dc"}.ivu-icon-ios-git-commit:before{content:"\f1dd"}.ivu-icon-ios-git-compare:before{content:"\f1de"}.ivu-icon-ios-git-merge:before{content:"\f1df"}.ivu-icon-ios-git-network:before{content:"\f1e0"}.ivu-icon-ios-git-pull-request:before{content:"\f1e1"}.ivu-icon-ios-glasses-outline:before{content:"\f1e2"}.ivu-icon-ios-glasses:before{content:"\f1e3"}.ivu-icon-ios-globe-outline:before{content:"\f1e4"}.ivu-icon-ios-globe:before{content:"\f1e5"}.ivu-icon-ios-grid-outline:before{content:"\f1e6"}.ivu-icon-ios-grid:before{content:"\f1e7"}.ivu-icon-ios-hammer-outline:before{content:"\f1e8"}.ivu-icon-ios-hammer:before{content:"\f1e9"}.ivu-icon-ios-hand-outline:before{content:"\f1ea"}.ivu-icon-ios-hand:before{content:"\f1eb"}.ivu-icon-ios-happy-outline:before{content:"\f1ec"}.ivu-icon-ios-happy:before{content:"\f1ed"}.ivu-icon-ios-headset-outline:before{content:"\f1ee"}.ivu-icon-ios-headset:before{content:"\f1ef"}.ivu-icon-ios-heart-outline:before{content:"\f1f0"}.ivu-icon-ios-heart:before{content:"\f1f1"}.ivu-icon-ios-help-buoy-outline:before{content:"\f1f2"}.ivu-icon-ios-help-buoy:before{content:"\f1f3"}.ivu-icon-ios-help-circle-outline:before{content:"\f1f4"}.ivu-icon-ios-help-circle:before{content:"\f1f5"}.ivu-icon-ios-help:before{content:"\f1f6"}.ivu-icon-ios-home-outline:before{content:"\f1f7"}.ivu-icon-ios-home:before{content:"\f1f8"}.ivu-icon-ios-ice-cream-outline:before{content:"\f1f9"}.ivu-icon-ios-ice-cream:before{content:"\f1fa"}.ivu-icon-ios-image-outline:before{content:"\f1fb"}.ivu-icon-ios-image:before{content:"\f1fc"}.ivu-icon-ios-images-outline:before{content:"\f1fd"}.ivu-icon-ios-images:before{content:"\f1fe"}.ivu-icon-ios-infinite-outline:before{content:"\f1ff"}.ivu-icon-ios-infinite:before{content:"\f200"}.ivu-icon-ios-information-circle-outline:before{content:"\f201"}.ivu-icon-ios-information-circle:before{content:"\f202"}.ivu-icon-ios-information:before{content:"\f203"}.ivu-icon-ios-ionic-outline:before{content:"\f204"}.ivu-icon-ios-ionic:before{content:"\f205"}.ivu-icon-ios-ionitron-outline:before{content:"\f206"}.ivu-icon-ios-ionitron:before{content:"\f207"}.ivu-icon-ios-jet-outline:before{content:"\f208"}.ivu-icon-ios-jet:before{content:"\f209"}.ivu-icon-ios-key-outline:before{content:"\f20a"}.ivu-icon-ios-key:before{content:"\f20b"}.ivu-icon-ios-keypad-outline:before{content:"\f20c"}.ivu-icon-ios-keypad:before{content:"\f20d"}.ivu-icon-ios-laptop:before{content:"\f20e"}.ivu-icon-ios-leaf-outline:before{content:"\f20f"}.ivu-icon-ios-leaf:before{content:"\f210"}.ivu-icon-ios-link-outline:before{content:"\f211"}.ivu-icon-ios-link:before{content:"\f212"}.ivu-icon-ios-list-box-outline:before{content:"\f213"}.ivu-icon-ios-list-box:before{content:"\f214"}.ivu-icon-ios-list:before{content:"\f215"}.ivu-icon-ios-locate-outline:before{content:"\f216"}.ivu-icon-ios-locate:before{content:"\f217"}.ivu-icon-ios-lock-outline:before{content:"\f218"}.ivu-icon-ios-lock:before{content:"\f219"}.ivu-icon-ios-log-in:before{content:"\f21a"}.ivu-icon-ios-log-out:before{content:"\f21b"}.ivu-icon-ios-magnet-outline:before{content:"\f21c"}.ivu-icon-ios-magnet:before{content:"\f21d"}.ivu-icon-ios-mail-open-outline:before{content:"\f21e"}.ivu-icon-ios-mail-open:before{content:"\f21f"}.ivu-icon-ios-mail-outline:before{content:"\f220"}.ivu-icon-ios-mail:before{content:"\f221"}.ivu-icon-ios-male:before{content:"\f222"}.ivu-icon-ios-man-outline:before{content:"\f223"}.ivu-icon-ios-man:before{content:"\f224"}.ivu-icon-ios-map-outline:before{content:"\f225"}.ivu-icon-ios-map:before{content:"\f226"}.ivu-icon-ios-medal-outline:before{content:"\f227"}.ivu-icon-ios-medal:before{content:"\f228"}.ivu-icon-ios-medical-outline:before{content:"\f229"}.ivu-icon-ios-medical:before{content:"\f22a"}.ivu-icon-ios-medkit-outline:before{content:"\f22b"}.ivu-icon-ios-medkit:before{content:"\f22c"}.ivu-icon-ios-megaphone-outline:before{content:"\f22d"}.ivu-icon-ios-megaphone:before{content:"\f22e"}.ivu-icon-ios-menu-outline:before{content:"\f22f"}.ivu-icon-ios-menu:before{content:"\f230"}.ivu-icon-ios-mic-off-outline:before{content:"\f231"}.ivu-icon-ios-mic-off:before{content:"\f232"}.ivu-icon-ios-mic-outline:before{content:"\f233"}.ivu-icon-ios-mic:before{content:"\f234"}.ivu-icon-ios-microphone-outline:before{content:"\f235"}.ivu-icon-ios-microphone:before{content:"\f236"}.ivu-icon-ios-moon-outline:before{content:"\f237"}.ivu-icon-ios-moon:before{content:"\f238"}.ivu-icon-ios-more-outline:before{content:"\f239"}.ivu-icon-ios-more:before{content:"\f23a"}.ivu-icon-ios-move:before{content:"\f23b"}.ivu-icon-ios-musical-note-outline:before{content:"\f23c"}.ivu-icon-ios-musical-note:before{content:"\f23d"}.ivu-icon-ios-musical-notes-outline:before{content:"\f23e"}.ivu-icon-ios-musical-notes:before{content:"\f23f"}.ivu-icon-ios-navigate-outline:before{content:"\f240"}.ivu-icon-ios-navigate:before{content:"\f241"}.ivu-icon-ios-no-smoking-outline:before{content:"\f242"}.ivu-icon-ios-no-smoking:before{content:"\f243"}.ivu-icon-ios-notifications-off-outline:before{content:"\f244"}.ivu-icon-ios-notifications-off:before{content:"\f245"}.ivu-icon-ios-notifications-outline:before{content:"\f246"}.ivu-icon-ios-notifications:before{content:"\f247"}.ivu-icon-ios-nuclear-outline:before{content:"\f248"}.ivu-icon-ios-nuclear:before{content:"\f249"}.ivu-icon-ios-nutrition-outline:before{content:"\f24a"}.ivu-icon-ios-nutrition:before{content:"\f24b"}.ivu-icon-ios-open-outline:before{content:"\f24c"}.ivu-icon-ios-open:before{content:"\f24d"}.ivu-icon-ios-options-outline:before{content:"\f24e"}.ivu-icon-ios-options:before{content:"\f24f"}.ivu-icon-ios-outlet-outline:before{content:"\f250"}.ivu-icon-ios-outlet:before{content:"\f251"}.ivu-icon-ios-paper-outline:before{content:"\f252"}.ivu-icon-ios-paper-plane-outline:before{content:"\f253"}.ivu-icon-ios-paper-plane:before{content:"\f254"}.ivu-icon-ios-paper:before{content:"\f255"}.ivu-icon-ios-partly-sunny-outline:before{content:"\f256"}.ivu-icon-ios-partly-sunny:before{content:"\f257"}.ivu-icon-ios-pause-outline:before{content:"\f258"}.ivu-icon-ios-pause:before{content:"\f259"}.ivu-icon-ios-paw-outline:before{content:"\f25a"}.ivu-icon-ios-paw:before{content:"\f25b"}.ivu-icon-ios-people-outline:before{content:"\f25c"}.ivu-icon-ios-people:before{content:"\f25d"}.ivu-icon-ios-person-add-outline:before{content:"\f25e"}.ivu-icon-ios-person-add:before{content:"\f25f"}.ivu-icon-ios-person-outline:before{content:"\f260"}.ivu-icon-ios-person:before{content:"\f261"}.ivu-icon-ios-phone-landscape:before{content:"\f262"}.ivu-icon-ios-phone-portrait:before{content:"\f263"}.ivu-icon-ios-photos-outline:before{content:"\f264"}.ivu-icon-ios-photos:before{content:"\f265"}.ivu-icon-ios-pie-outline:before{content:"\f266"}.ivu-icon-ios-pie:before{content:"\f267"}.ivu-icon-ios-pin-outline:before{content:"\f268"}.ivu-icon-ios-pin:before{content:"\f269"}.ivu-icon-ios-pint-outline:before{content:"\f26a"}.ivu-icon-ios-pint:before{content:"\f26b"}.ivu-icon-ios-pizza-outline:before{content:"\f26c"}.ivu-icon-ios-pizza:before{content:"\f26d"}.ivu-icon-ios-plane-outline:before{content:"\f26e"}.ivu-icon-ios-plane:before{content:"\f26f"}.ivu-icon-ios-planet-outline:before{content:"\f270"}.ivu-icon-ios-planet:before{content:"\f271"}.ivu-icon-ios-play-outline:before{content:"\f272"}.ivu-icon-ios-play:before{content:"\f273"}.ivu-icon-ios-podium-outline:before{content:"\f274"}.ivu-icon-ios-podium:before{content:"\f275"}.ivu-icon-ios-power-outline:before{content:"\f276"}.ivu-icon-ios-power:before{content:"\f277"}.ivu-icon-ios-pricetag-outline:before{content:"\f278"}.ivu-icon-ios-pricetag:before{content:"\f279"}.ivu-icon-ios-pricetags-outline:before{content:"\f27a"}.ivu-icon-ios-pricetags:before{content:"\f27b"}.ivu-icon-ios-print-outline:before{content:"\f27c"}.ivu-icon-ios-print:before{content:"\f27d"}.ivu-icon-ios-pulse-outline:before{content:"\f27e"}.ivu-icon-ios-pulse:before{content:"\f27f"}.ivu-icon-ios-qr-scanner:before{content:"\f280"}.ivu-icon-ios-quote-outline:before{content:"\f281"}.ivu-icon-ios-quote:before{content:"\f282"}.ivu-icon-ios-radio-button-off:before{content:"\f283"}.ivu-icon-ios-radio-button-on:before{content:"\f284"}.ivu-icon-ios-radio-outline:before{content:"\f285"}.ivu-icon-ios-radio:before{content:"\f286"}.ivu-icon-ios-rainy-outline:before{content:"\f287"}.ivu-icon-ios-rainy:before{content:"\f288"}.ivu-icon-ios-recording-outline:before{content:"\f289"}.ivu-icon-ios-recording:before{content:"\f28a"}.ivu-icon-ios-redo-outline:before{content:"\f28b"}.ivu-icon-ios-redo:before{content:"\f28c"}.ivu-icon-ios-refresh-circle-outline:before{content:"\f28d"}.ivu-icon-ios-refresh-circle:before{content:"\f28e"}.ivu-icon-ios-refresh:before{content:"\f28f"}.ivu-icon-ios-remove-circle-outline:before{content:"\f290"}.ivu-icon-ios-remove-circle:before{content:"\f291"}.ivu-icon-ios-remove:before{content:"\f292"}.ivu-icon-ios-reorder:before{content:"\f293"}.ivu-icon-ios-repeat:before{content:"\f294"}.ivu-icon-ios-resize:before{content:"\f295"}.ivu-icon-ios-restaurant-outline:before{content:"\f296"}.ivu-icon-ios-restaurant:before{content:"\f297"}.ivu-icon-ios-return-left:before{content:"\f298"}.ivu-icon-ios-return-right:before{content:"\f299"}.ivu-icon-ios-reverse-camera-outline:before{content:"\f29a"}.ivu-icon-ios-reverse-camera:before{content:"\f29b"}.ivu-icon-ios-rewind-outline:before{content:"\f29c"}.ivu-icon-ios-rewind:before{content:"\f29d"}.ivu-icon-ios-ribbon-outline:before{content:"\f29e"}.ivu-icon-ios-ribbon:before{content:"\f29f"}.ivu-icon-ios-rose-outline:before{content:"\f2a0"}.ivu-icon-ios-rose:before{content:"\f2a1"}.ivu-icon-ios-sad-outline:before{content:"\f2a2"}.ivu-icon-ios-sad:before{content:"\f2a3"}.ivu-icon-ios-school-outline:before{content:"\f2a4"}.ivu-icon-ios-school:before{content:"\f2a5"}.ivu-icon-ios-search-outline:before{content:"\f2a6"}.ivu-icon-ios-search:before{content:"\f2a7"}.ivu-icon-ios-send-outline:before{content:"\f2a8"}.ivu-icon-ios-send:before{content:"\f2a9"}.ivu-icon-ios-settings-outline:before{content:"\f2aa"}.ivu-icon-ios-settings:before{content:"\f2ab"}.ivu-icon-ios-share-alt-outline:before{content:"\f2ac"}.ivu-icon-ios-share-alt:before{content:"\f2ad"}.ivu-icon-ios-share-outline:before{content:"\f2ae"}.ivu-icon-ios-share:before{content:"\f2af"}.ivu-icon-ios-shirt-outline:before{content:"\f2b0"}.ivu-icon-ios-shirt:before{content:"\f2b1"}.ivu-icon-ios-shuffle:before{content:"\f2b2"}.ivu-icon-ios-skip-backward-outline:before{content:"\f2b3"}.ivu-icon-ios-skip-backward:before{content:"\f2b4"}.ivu-icon-ios-skip-forward-outline:before{content:"\f2b5"}.ivu-icon-ios-skip-forward:before{content:"\f2b6"}.ivu-icon-ios-snow-outline:before{content:"\f2b7"}.ivu-icon-ios-snow:before{content:"\f2b8"}.ivu-icon-ios-speedometer-outline:before{content:"\f2b9"}.ivu-icon-ios-speedometer:before{content:"\f2ba"}.ivu-icon-ios-square-outline:before{content:"\f2bb"}.ivu-icon-ios-square:before{content:"\f2bc"}.ivu-icon-ios-star-half:before{content:"\f2bd"}.ivu-icon-ios-star-outline:before{content:"\f2be"}.ivu-icon-ios-star:before{content:"\f2bf"}.ivu-icon-ios-stats-outline:before{content:"\f2c0"}.ivu-icon-ios-stats:before{content:"\f2c1"}.ivu-icon-ios-stopwatch-outline:before{content:"\f2c2"}.ivu-icon-ios-stopwatch:before{content:"\f2c3"}.ivu-icon-ios-subway-outline:before{content:"\f2c4"}.ivu-icon-ios-subway:before{content:"\f2c5"}.ivu-icon-ios-sunny-outline:before{content:"\f2c6"}.ivu-icon-ios-sunny:before{content:"\f2c7"}.ivu-icon-ios-swap:before{content:"\f2c8"}.ivu-icon-ios-switch-outline:before{content:"\f2c9"}.ivu-icon-ios-switch:before{content:"\f2ca"}.ivu-icon-ios-sync:before{content:"\f2cb"}.ivu-icon-ios-tablet-landscape:before{content:"\f2cc"}.ivu-icon-ios-tablet-portrait:before{content:"\f2cd"}.ivu-icon-ios-tennisball-outline:before{content:"\f2ce"}.ivu-icon-ios-tennisball:before{content:"\f2cf"}.ivu-icon-ios-text-outline:before{content:"\f2d0"}.ivu-icon-ios-text:before{content:"\f2d1"}.ivu-icon-ios-thermometer-outline:before{content:"\f2d2"}.ivu-icon-ios-thermometer:before{content:"\f2d3"}.ivu-icon-ios-thumbs-down-outline:before{content:"\f2d4"}.ivu-icon-ios-thumbs-down:before{content:"\f2d5"}.ivu-icon-ios-thumbs-up-outline:before{content:"\f2d6"}.ivu-icon-ios-thumbs-up:before{content:"\f2d7"}.ivu-icon-ios-thunderstorm-outline:before{content:"\f2d8"}.ivu-icon-ios-thunderstorm:before{content:"\f2d9"}.ivu-icon-ios-time-outline:before{content:"\f2da"}.ivu-icon-ios-time:before{content:"\f2db"}.ivu-icon-ios-timer-outline:before{content:"\f2dc"}.ivu-icon-ios-timer:before{content:"\f2dd"}.ivu-icon-ios-train-outline:before{content:"\f2de"}.ivu-icon-ios-train:before{content:"\f2df"}.ivu-icon-ios-transgender:before{content:"\f2e0"}.ivu-icon-ios-trash-outline:before{content:"\f2e1"}.ivu-icon-ios-trash:before{content:"\f2e2"}.ivu-icon-ios-trending-down:before{content:"\f2e3"}.ivu-icon-ios-trending-up:before{content:"\f2e4"}.ivu-icon-ios-trophy-outline:before{content:"\f2e5"}.ivu-icon-ios-trophy:before{content:"\f2e6"}.ivu-icon-ios-umbrella-outline:before{content:"\f2e7"}.ivu-icon-ios-umbrella:before{content:"\f2e8"}.ivu-icon-ios-undo-outline:before{content:"\f2e9"}.ivu-icon-ios-undo:before{content:"\f2ea"}.ivu-icon-ios-unlock-outline:before{content:"\f2eb"}.ivu-icon-ios-unlock:before{content:"\f2ec"}.ivu-icon-ios-videocam-outline:before{content:"\f2ed"}.ivu-icon-ios-videocam:before{content:"\f2ee"}.ivu-icon-ios-volume-down:before{content:"\f2ef"}.ivu-icon-ios-volume-mute:before{content:"\f2f0"}.ivu-icon-ios-volume-off:before{content:"\f2f1"}.ivu-icon-ios-volume-up:before{content:"\f2f2"}.ivu-icon-ios-walk:before{content:"\f2f3"}.ivu-icon-ios-warning-outline:before{content:"\f2f4"}.ivu-icon-ios-warning:before{content:"\f2f5"}.ivu-icon-ios-watch:before{content:"\f2f6"}.ivu-icon-ios-water-outline:before{content:"\f2f7"}.ivu-icon-ios-water:before{content:"\f2f8"}.ivu-icon-ios-wifi-outline:before{content:"\f2f9"}.ivu-icon-ios-wifi:before{content:"\f2fa"}.ivu-icon-ios-wine-outline:before{content:"\f2fb"}.ivu-icon-ios-wine:before{content:"\f2fc"}.ivu-icon-ios-woman-outline:before{content:"\f2fd"}.ivu-icon-ios-woman:before{content:"\f2fe"}.ivu-icon-logo-android:before{content:"\f2ff"}.ivu-icon-logo-angular:before{content:"\f300"}.ivu-icon-logo-apple:before{content:"\f301"}.ivu-icon-logo-bitcoin:before{content:"\f302"}.ivu-icon-logo-buffer:before{content:"\f303"}.ivu-icon-logo-chrome:before{content:"\f304"}.ivu-icon-logo-codepen:before{content:"\f305"}.ivu-icon-logo-css3:before{content:"\f306"}.ivu-icon-logo-designernews:before{content:"\f307"}.ivu-icon-logo-dribbble:before{content:"\f308"}.ivu-icon-logo-dropbox:before{content:"\f309"}.ivu-icon-logo-euro:before{content:"\f30a"}.ivu-icon-logo-facebook:before{content:"\f30b"}.ivu-icon-logo-foursquare:before{content:"\f30c"}.ivu-icon-logo-freebsd-devil:before{content:"\f30d"}.ivu-icon-logo-github:before{content:"\f30e"}.ivu-icon-logo-google:before{content:"\f30f"}.ivu-icon-logo-googleplus:before{content:"\f310"}.ivu-icon-logo-hackernews:before{content:"\f311"}.ivu-icon-logo-html5:before{content:"\f312"}.ivu-icon-logo-instagram:before{content:"\f313"}.ivu-icon-logo-javascript:before{content:"\f314"}.ivu-icon-logo-linkedin:before{content:"\f315"}.ivu-icon-logo-markdown:before{content:"\f316"}.ivu-icon-logo-nodejs:before{content:"\f317"}.ivu-icon-logo-octocat:before{content:"\f318"}.ivu-icon-logo-pinterest:before{content:"\f319"}.ivu-icon-logo-playstation:before{content:"\f31a"}.ivu-icon-logo-python:before{content:"\f31b"}.ivu-icon-logo-reddit:before{content:"\f31c"}.ivu-icon-logo-rss:before{content:"\f31d"}.ivu-icon-logo-sass:before{content:"\f31e"}.ivu-icon-logo-skype:before{content:"\f31f"}.ivu-icon-logo-snapchat:before{content:"\f320"}.ivu-icon-logo-steam:before{content:"\f321"}.ivu-icon-logo-tumblr:before{content:"\f322"}.ivu-icon-logo-tux:before{content:"\f323"}.ivu-icon-logo-twitch:before{content:"\f324"}.ivu-icon-logo-twitter:before{content:"\f325"}.ivu-icon-logo-usd:before{content:"\f326"}.ivu-icon-logo-vimeo:before{content:"\f327"}.ivu-icon-logo-whatsapp:before{content:"\f328"}.ivu-icon-logo-windows:before{content:"\f329"}.ivu-icon-logo-wordpress:before{content:"\f32a"}.ivu-icon-logo-xbox:before{content:"\f32b"}.ivu-icon-logo-yahoo:before{content:"\f32c"}.ivu-icon-logo-yen:before{content:"\f32d"}.ivu-icon-logo-youtube:before{content:"\f32e"}.ivu-icon-md-add-circle:before{content:"\f32f"}.ivu-icon-md-add:before{content:"\f330"}.ivu-icon-md-alarm:before{content:"\f331"}.ivu-icon-md-albums:before{content:"\f332"}.ivu-icon-md-alert:before{content:"\f333"}.ivu-icon-md-american-football:before{content:"\f334"}.ivu-icon-md-analytics:before{content:"\f335"}.ivu-icon-md-aperture:before{content:"\f336"}.ivu-icon-md-apps:before{content:"\f337"}.ivu-icon-md-appstore:before{content:"\f338"}.ivu-icon-md-archive:before{content:"\f339"}.ivu-icon-md-arrow-back:before{content:"\f33a"}.ivu-icon-md-arrow-down:before{content:"\f33b"}.ivu-icon-md-arrow-dropdown-circle:before{content:"\f33c"}.ivu-icon-md-arrow-dropdown:before{content:"\f33d"}.ivu-icon-md-arrow-dropleft-circle:before{content:"\f33e"}.ivu-icon-md-arrow-dropleft:before{content:"\f33f"}.ivu-icon-md-arrow-dropright-circle:before{content:"\f340"}.ivu-icon-md-arrow-dropright:before{content:"\f341"}.ivu-icon-md-arrow-dropup-circle:before{content:"\f342"}.ivu-icon-md-arrow-dropup:before{content:"\f343"}.ivu-icon-md-arrow-forward:before{content:"\f344"}.ivu-icon-md-arrow-round-back:before{content:"\f345"}.ivu-icon-md-arrow-round-down:before{content:"\f346"}.ivu-icon-md-arrow-round-forward:before{content:"\f347"}.ivu-icon-md-arrow-round-up:before{content:"\f348"}.ivu-icon-md-arrow-up:before{content:"\f349"}.ivu-icon-md-at:before{content:"\f34a"}.ivu-icon-md-attach:before{content:"\f34b"}.ivu-icon-md-backspace:before{content:"\f34c"}.ivu-icon-md-barcode:before{content:"\f34d"}.ivu-icon-md-baseball:before{content:"\f34e"}.ivu-icon-md-basket:before{content:"\f34f"}.ivu-icon-md-basketball:before{content:"\f350"}.ivu-icon-md-battery-charging:before{content:"\f351"}.ivu-icon-md-battery-dead:before{content:"\f352"}.ivu-icon-md-battery-full:before{content:"\f353"}.ivu-icon-md-beaker:before{content:"\f354"}.ivu-icon-md-beer:before{content:"\f355"}.ivu-icon-md-bicycle:before{content:"\f356"}.ivu-icon-md-bluetooth:before{content:"\f357"}.ivu-icon-md-boat:before{content:"\f358"}.ivu-icon-md-body:before{content:"\f359"}.ivu-icon-md-bonfire:before{content:"\f35a"}.ivu-icon-md-book:before{content:"\f35b"}.ivu-icon-md-bookmark:before{content:"\f35c"}.ivu-icon-md-bookmarks:before{content:"\f35d"}.ivu-icon-md-bowtie:before{content:"\f35e"}.ivu-icon-md-briefcase:before{content:"\f35f"}.ivu-icon-md-browsers:before{content:"\f360"}.ivu-icon-md-brush:before{content:"\f361"}.ivu-icon-md-bug:before{content:"\f362"}.ivu-icon-md-build:before{content:"\f363"}.ivu-icon-md-bulb:before{content:"\f364"}.ivu-icon-md-bus:before{content:"\f365"}.ivu-icon-md-cafe:before{content:"\f366"}.ivu-icon-md-calculator:before{content:"\f367"}.ivu-icon-md-calendar:before{content:"\f368"}.ivu-icon-md-call:before{content:"\f369"}.ivu-icon-md-camera:before{content:"\f36a"}.ivu-icon-md-car:before{content:"\f36b"}.ivu-icon-md-card:before{content:"\f36c"}.ivu-icon-md-cart:before{content:"\f36d"}.ivu-icon-md-cash:before{content:"\f36e"}.ivu-icon-md-chatboxes:before{content:"\f36f"}.ivu-icon-md-chatbubbles:before{content:"\f370"}.ivu-icon-md-checkbox-outline:before{content:"\f371"}.ivu-icon-md-checkbox:before{content:"\f372"}.ivu-icon-md-checkmark-circle-outline:before{content:"\f373"}.ivu-icon-md-checkmark-circle:before{content:"\f374"}.ivu-icon-md-checkmark:before{content:"\f375"}.ivu-icon-md-clipboard:before{content:"\f376"}.ivu-icon-md-clock:before{content:"\f377"}.ivu-icon-md-close-circle:before{content:"\f378"}.ivu-icon-md-close:before{content:"\f379"}.ivu-icon-md-closed-captioning:before{content:"\f37a"}.ivu-icon-md-cloud-circle:before{content:"\f37b"}.ivu-icon-md-cloud-done:before{content:"\f37c"}.ivu-icon-md-cloud-download:before{content:"\f37d"}.ivu-icon-md-cloud-outline:before{content:"\f37e"}.ivu-icon-md-cloud-upload:before{content:"\f37f"}.ivu-icon-md-cloud:before{content:"\f380"}.ivu-icon-md-cloudy-night:before{content:"\f381"}.ivu-icon-md-cloudy:before{content:"\f382"}.ivu-icon-md-code-download:before{content:"\f383"}.ivu-icon-md-code-working:before{content:"\f384"}.ivu-icon-md-code:before{content:"\f385"}.ivu-icon-md-cog:before{content:"\f386"}.ivu-icon-md-color-fill:before{content:"\f387"}.ivu-icon-md-color-filter:before{content:"\f388"}.ivu-icon-md-color-palette:before{content:"\f389"}.ivu-icon-md-color-wand:before{content:"\f38a"}.ivu-icon-md-compass:before{content:"\f38b"}.ivu-icon-md-construct:before{content:"\f38c"}.ivu-icon-md-contact:before{content:"\f38d"}.ivu-icon-md-contacts:before{content:"\f38e"}.ivu-icon-md-contract:before{content:"\f38f"}.ivu-icon-md-contrast:before{content:"\f390"}.ivu-icon-md-copy:before{content:"\f391"}.ivu-icon-md-create:before{content:"\f392"}.ivu-icon-md-crop:before{content:"\f393"}.ivu-icon-md-cube:before{content:"\f394"}.ivu-icon-md-cut:before{content:"\f395"}.ivu-icon-md-desktop:before{content:"\f396"}.ivu-icon-md-disc:before{content:"\f397"}.ivu-icon-md-document:before{content:"\f398"}.ivu-icon-md-done-all:before{content:"\f399"}.ivu-icon-md-download:before{content:"\f39a"}.ivu-icon-md-easel:before{content:"\f39b"}.ivu-icon-md-egg:before{content:"\f39c"}.ivu-icon-md-exit:before{content:"\f39d"}.ivu-icon-md-expand:before{content:"\f39e"}.ivu-icon-md-eye-off:before{content:"\f39f"}.ivu-icon-md-eye:before{content:"\f3a0"}.ivu-icon-md-fastforward:before{content:"\f3a1"}.ivu-icon-md-female:before{content:"\f3a2"}.ivu-icon-md-filing:before{content:"\f3a3"}.ivu-icon-md-film:before{content:"\f3a4"}.ivu-icon-md-finger-print:before{content:"\f3a5"}.ivu-icon-md-flag:before{content:"\f3a6"}.ivu-icon-md-flame:before{content:"\f3a7"}.ivu-icon-md-flash:before{content:"\f3a8"}.ivu-icon-md-flask:before{content:"\f3a9"}.ivu-icon-md-flower:before{content:"\f3aa"}.ivu-icon-md-folder-open:before{content:"\f3ab"}.ivu-icon-md-folder:before{content:"\f3ac"}.ivu-icon-md-football:before{content:"\f3ad"}.ivu-icon-md-funnel:before{content:"\f3ae"}.ivu-icon-md-game-controller-a:before{content:"\f3af"}.ivu-icon-md-game-controller-b:before{content:"\f3b0"}.ivu-icon-md-git-branch:before{content:"\f3b1"}.ivu-icon-md-git-commit:before{content:"\f3b2"}.ivu-icon-md-git-compare:before{content:"\f3b3"}.ivu-icon-md-git-merge:before{content:"\f3b4"}.ivu-icon-md-git-network:before{content:"\f3b5"}.ivu-icon-md-git-pull-request:before{content:"\f3b6"}.ivu-icon-md-glasses:before{content:"\f3b7"}.ivu-icon-md-globe:before{content:"\f3b8"}.ivu-icon-md-grid:before{content:"\f3b9"}.ivu-icon-md-hammer:before{content:"\f3ba"}.ivu-icon-md-hand:before{content:"\f3bb"}.ivu-icon-md-happy:before{content:"\f3bc"}.ivu-icon-md-headset:before{content:"\f3bd"}.ivu-icon-md-heart-outline:before{content:"\f3be"}.ivu-icon-md-heart:before{content:"\f3bf"}.ivu-icon-md-help-buoy:before{content:"\f3c0"}.ivu-icon-md-help-circle:before{content:"\f3c1"}.ivu-icon-md-help:before{content:"\f3c2"}.ivu-icon-md-home:before{content:"\f3c3"}.ivu-icon-md-ice-cream:before{content:"\f3c4"}.ivu-icon-md-image:before{content:"\f3c5"}.ivu-icon-md-images:before{content:"\f3c6"}.ivu-icon-md-infinite:before{content:"\f3c7"}.ivu-icon-md-information-circle:before{content:"\f3c8"}.ivu-icon-md-information:before{content:"\f3c9"}.ivu-icon-md-ionic:before{content:"\f3ca"}.ivu-icon-md-ionitron:before{content:"\f3cb"}.ivu-icon-md-jet:before{content:"\f3cc"}.ivu-icon-md-key:before{content:"\f3cd"}.ivu-icon-md-keypad:before{content:"\f3ce"}.ivu-icon-md-laptop:before{content:"\f3cf"}.ivu-icon-md-leaf:before{content:"\f3d0"}.ivu-icon-md-link:before{content:"\f3d1"}.ivu-icon-md-list-box:before{content:"\f3d2"}.ivu-icon-md-list:before{content:"\f3d3"}.ivu-icon-md-locate:before{content:"\f3d4"}.ivu-icon-md-lock:before{content:"\f3d5"}.ivu-icon-md-log-in:before{content:"\f3d6"}.ivu-icon-md-log-out:before{content:"\f3d7"}.ivu-icon-md-magnet:before{content:"\f3d8"}.ivu-icon-md-mail-open:before{content:"\f3d9"}.ivu-icon-md-mail:before{content:"\f3da"}.ivu-icon-md-male:before{content:"\f3db"}.ivu-icon-md-man:before{content:"\f3dc"}.ivu-icon-md-map:before{content:"\f3dd"}.ivu-icon-md-medal:before{content:"\f3de"}.ivu-icon-md-medical:before{content:"\f3df"}.ivu-icon-md-medkit:before{content:"\f3e0"}.ivu-icon-md-megaphone:before{content:"\f3e1"}.ivu-icon-md-menu:before{content:"\f3e2"}.ivu-icon-md-mic-off:before{content:"\f3e3"}.ivu-icon-md-mic:before{content:"\f3e4"}.ivu-icon-md-microphone:before{content:"\f3e5"}.ivu-icon-md-moon:before{content:"\f3e6"}.ivu-icon-md-more:before{content:"\f3e7"}.ivu-icon-md-move:before{content:"\f3e8"}.ivu-icon-md-musical-note:before{content:"\f3e9"}.ivu-icon-md-musical-notes:before{content:"\f3ea"}.ivu-icon-md-navigate:before{content:"\f3eb"}.ivu-icon-md-no-smoking:before{content:"\f3ec"}.ivu-icon-md-notifications-off:before{content:"\f3ed"}.ivu-icon-md-notifications-outline:before{content:"\f3ee"}.ivu-icon-md-notifications:before{content:"\f3ef"}.ivu-icon-md-nuclear:before{content:"\f3f0"}.ivu-icon-md-nutrition:before{content:"\f3f1"}.ivu-icon-md-open:before{content:"\f3f2"}.ivu-icon-md-options:before{content:"\f3f3"}.ivu-icon-md-outlet:before{content:"\f3f4"}.ivu-icon-md-paper-plane:before{content:"\f3f5"}.ivu-icon-md-paper:before{content:"\f3f6"}.ivu-icon-md-partly-sunny:before{content:"\f3f7"}.ivu-icon-md-pause:before{content:"\f3f8"}.ivu-icon-md-paw:before{content:"\f3f9"}.ivu-icon-md-people:before{content:"\f3fa"}.ivu-icon-md-person-add:before{content:"\f3fb"}.ivu-icon-md-person:before{content:"\f3fc"}.ivu-icon-md-phone-landscape:before{content:"\f3fd"}.ivu-icon-md-phone-portrait:before{content:"\f3fe"}.ivu-icon-md-photos:before{content:"\f3ff"}.ivu-icon-md-pie:before{content:"\f400"}.ivu-icon-md-pin:before{content:"\f401"}.ivu-icon-md-pint:before{content:"\f402"}.ivu-icon-md-pizza:before{content:"\f403"}.ivu-icon-md-plane:before{content:"\f404"}.ivu-icon-md-planet:before{content:"\f405"}.ivu-icon-md-play:before{content:"\f406"}.ivu-icon-md-podium:before{content:"\f407"}.ivu-icon-md-power:before{content:"\f408"}.ivu-icon-md-pricetag:before{content:"\f409"}.ivu-icon-md-pricetags:before{content:"\f40a"}.ivu-icon-md-print:before{content:"\f40b"}.ivu-icon-md-pulse:before{content:"\f40c"}.ivu-icon-md-qr-scanner:before{content:"\f40d"}.ivu-icon-md-quote:before{content:"\f40e"}.ivu-icon-md-radio-button-off:before{content:"\f40f"}.ivu-icon-md-radio-button-on:before{content:"\f410"}.ivu-icon-md-radio:before{content:"\f411"}.ivu-icon-md-rainy:before{content:"\f412"}.ivu-icon-md-recording:before{content:"\f413"}.ivu-icon-md-redo:before{content:"\f414"}.ivu-icon-md-refresh-circle:before{content:"\f415"}.ivu-icon-md-refresh:before{content:"\f416"}.ivu-icon-md-remove-circle:before{content:"\f417"}.ivu-icon-md-remove:before{content:"\f418"}.ivu-icon-md-reorder:before{content:"\f419"}.ivu-icon-md-repeat:before{content:"\f41a"}.ivu-icon-md-resize:before{content:"\f41b"}.ivu-icon-md-restaurant:before{content:"\f41c"}.ivu-icon-md-return-left:before{content:"\f41d"}.ivu-icon-md-return-right:before{content:"\f41e"}.ivu-icon-md-reverse-camera:before{content:"\f41f"}.ivu-icon-md-rewind:before{content:"\f420"}.ivu-icon-md-ribbon:before{content:"\f421"}.ivu-icon-md-rose:before{content:"\f422"}.ivu-icon-md-sad:before{content:"\f423"}.ivu-icon-md-school:before{content:"\f424"}.ivu-icon-md-search:before{content:"\f425"}.ivu-icon-md-send:before{content:"\f426"}.ivu-icon-md-settings:before{content:"\f427"}.ivu-icon-md-share-alt:before{content:"\f428"}.ivu-icon-md-share:before{content:"\f429"}.ivu-icon-md-shirt:before{content:"\f42a"}.ivu-icon-md-shuffle:before{content:"\f42b"}.ivu-icon-md-skip-backward:before{content:"\f42c"}.ivu-icon-md-skip-forward:before{content:"\f42d"}.ivu-icon-md-snow:before{content:"\f42e"}.ivu-icon-md-speedometer:before{content:"\f42f"}.ivu-icon-md-square-outline:before{content:"\f430"}.ivu-icon-md-square:before{content:"\f431"}.ivu-icon-md-star-half:before{content:"\f432"}.ivu-icon-md-star-outline:before{content:"\f433"}.ivu-icon-md-star:before{content:"\f434"}.ivu-icon-md-stats:before{content:"\f435"}.ivu-icon-md-stopwatch:before{content:"\f436"}.ivu-icon-md-subway:before{content:"\f437"}.ivu-icon-md-sunny:before{content:"\f438"}.ivu-icon-md-swap:before{content:"\f439"}.ivu-icon-md-switch:before{content:"\f43a"}.ivu-icon-md-sync:before{content:"\f43b"}.ivu-icon-md-tablet-landscape:before{content:"\f43c"}.ivu-icon-md-tablet-portrait:before{content:"\f43d"}.ivu-icon-md-tennisball:before{content:"\f43e"}.ivu-icon-md-text:before{content:"\f43f"}.ivu-icon-md-thermometer:before{content:"\f440"}.ivu-icon-md-thumbs-down:before{content:"\f441"}.ivu-icon-md-thumbs-up:before{content:"\f442"}.ivu-icon-md-thunderstorm:before{content:"\f443"}.ivu-icon-md-time:before{content:"\f444"}.ivu-icon-md-timer:before{content:"\f445"}.ivu-icon-md-train:before{content:"\f446"}.ivu-icon-md-transgender:before{content:"\f447"}.ivu-icon-md-trash:before{content:"\f448"}.ivu-icon-md-trending-down:before{content:"\f449"}.ivu-icon-md-trending-up:before{content:"\f44a"}.ivu-icon-md-trophy:before{content:"\f44b"}.ivu-icon-md-umbrella:before{content:"\f44c"}.ivu-icon-md-undo:before{content:"\f44d"}.ivu-icon-md-unlock:before{content:"\f44e"}.ivu-icon-md-videocam:before{content:"\f44f"}.ivu-icon-md-volume-down:before{content:"\f450"}.ivu-icon-md-volume-mute:before{content:"\f451"}.ivu-icon-md-volume-off:before{content:"\f452"}.ivu-icon-md-volume-up:before{content:"\f453"}.ivu-icon-md-walk:before{content:"\f454"}.ivu-icon-md-warning:before{content:"\f455"}.ivu-icon-md-watch:before{content:"\f456"}.ivu-icon-md-water:before{content:"\f457"}.ivu-icon-md-wifi:before{content:"\f458"}.ivu-icon-md-wine:before{content:"\f459"}.ivu-icon-md-woman:before{content:"\f45a"}.ivu-icon-ios-loading:before{content:"\f45b"}.ivu-row{position:relative;margin-left:0;margin-right:0;height:auto;zoom:1;display:block}.ivu-row:after,.ivu-row:before{content:"";display:table}.ivu-row:after{clear:both;visibility:hidden;font-size:0;height:0}.ivu-row-flex{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap}.ivu-row-flex:after,.ivu-row-flex:before{display:-webkit-box;display:-ms-flexbox;display:flex}.ivu-row-flex-start{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.ivu-row-flex-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.ivu-row-flex-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.ivu-row-flex-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.ivu-row-flex-space-around{-ms-flex-pack:distribute;justify-content:space-around}.ivu-row-flex-top{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.ivu-row-flex-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ivu-row-flex-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.ivu-col{position:relative;display:block}.ivu-col-span-1,.ivu-col-span-10,.ivu-col-span-11,.ivu-col-span-12,.ivu-col-span-13,.ivu-col-span-14,.ivu-col-span-15,.ivu-col-span-16,.ivu-col-span-17,.ivu-col-span-18,.ivu-col-span-19,.ivu-col-span-2,.ivu-col-span-20,.ivu-col-span-21,.ivu-col-span-22,.ivu-col-span-23,.ivu-col-span-24,.ivu-col-span-3,.ivu-col-span-4,.ivu-col-span-5,.ivu-col-span-6,.ivu-col-span-7,.ivu-col-span-8,.ivu-col-span-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-24{display:block;width:100%}.ivu-col-push-24{left:100%}.ivu-col-pull-24{right:100%}.ivu-col-offset-24{margin-left:100%}.ivu-col-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-23{display:block;width:95.83333333%}.ivu-col-push-23{left:95.83333333%}.ivu-col-pull-23{right:95.83333333%}.ivu-col-offset-23{margin-left:95.83333333%}.ivu-col-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-22{display:block;width:91.66666667%}.ivu-col-push-22{left:91.66666667%}.ivu-col-pull-22{right:91.66666667%}.ivu-col-offset-22{margin-left:91.66666667%}.ivu-col-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-21{display:block;width:87.5%}.ivu-col-push-21{left:87.5%}.ivu-col-pull-21{right:87.5%}.ivu-col-offset-21{margin-left:87.5%}.ivu-col-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-20{display:block;width:83.33333333%}.ivu-col-push-20{left:83.33333333%}.ivu-col-pull-20{right:83.33333333%}.ivu-col-offset-20{margin-left:83.33333333%}.ivu-col-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-19{display:block;width:79.16666667%}.ivu-col-push-19{left:79.16666667%}.ivu-col-pull-19{right:79.16666667%}.ivu-col-offset-19{margin-left:79.16666667%}.ivu-col-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-18{display:block;width:75%}.ivu-col-push-18{left:75%}.ivu-col-pull-18{right:75%}.ivu-col-offset-18{margin-left:75%}.ivu-col-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-17{display:block;width:70.83333333%}.ivu-col-push-17{left:70.83333333%}.ivu-col-pull-17{right:70.83333333%}.ivu-col-offset-17{margin-left:70.83333333%}.ivu-col-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-16{display:block;width:66.66666667%}.ivu-col-push-16{left:66.66666667%}.ivu-col-pull-16{right:66.66666667%}.ivu-col-offset-16{margin-left:66.66666667%}.ivu-col-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-15{display:block;width:62.5%}.ivu-col-push-15{left:62.5%}.ivu-col-pull-15{right:62.5%}.ivu-col-offset-15{margin-left:62.5%}.ivu-col-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-14{display:block;width:58.33333333%}.ivu-col-push-14{left:58.33333333%}.ivu-col-pull-14{right:58.33333333%}.ivu-col-offset-14{margin-left:58.33333333%}.ivu-col-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-13{display:block;width:54.16666667%}.ivu-col-push-13{left:54.16666667%}.ivu-col-pull-13{right:54.16666667%}.ivu-col-offset-13{margin-left:54.16666667%}.ivu-col-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-12{display:block;width:50%}.ivu-col-push-12{left:50%}.ivu-col-pull-12{right:50%}.ivu-col-offset-12{margin-left:50%}.ivu-col-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-11{display:block;width:45.83333333%}.ivu-col-push-11{left:45.83333333%}.ivu-col-pull-11{right:45.83333333%}.ivu-col-offset-11{margin-left:45.83333333%}.ivu-col-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-10{display:block;width:41.66666667%}.ivu-col-push-10{left:41.66666667%}.ivu-col-pull-10{right:41.66666667%}.ivu-col-offset-10{margin-left:41.66666667%}.ivu-col-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-9{display:block;width:37.5%}.ivu-col-push-9{left:37.5%}.ivu-col-pull-9{right:37.5%}.ivu-col-offset-9{margin-left:37.5%}.ivu-col-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-8{display:block;width:33.33333333%}.ivu-col-push-8{left:33.33333333%}.ivu-col-pull-8{right:33.33333333%}.ivu-col-offset-8{margin-left:33.33333333%}.ivu-col-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-7{display:block;width:29.16666667%}.ivu-col-push-7{left:29.16666667%}.ivu-col-pull-7{right:29.16666667%}.ivu-col-offset-7{margin-left:29.16666667%}.ivu-col-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-6{display:block;width:25%}.ivu-col-push-6{left:25%}.ivu-col-pull-6{right:25%}.ivu-col-offset-6{margin-left:25%}.ivu-col-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-5{display:block;width:20.83333333%}.ivu-col-push-5{left:20.83333333%}.ivu-col-pull-5{right:20.83333333%}.ivu-col-offset-5{margin-left:20.83333333%}.ivu-col-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-4{display:block;width:16.66666667%}.ivu-col-push-4{left:16.66666667%}.ivu-col-pull-4{right:16.66666667%}.ivu-col-offset-4{margin-left:16.66666667%}.ivu-col-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-3{display:block;width:12.5%}.ivu-col-push-3{left:12.5%}.ivu-col-pull-3{right:12.5%}.ivu-col-offset-3{margin-left:12.5%}.ivu-col-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-2{display:block;width:8.33333333%}.ivu-col-push-2{left:8.33333333%}.ivu-col-pull-2{right:8.33333333%}.ivu-col-offset-2{margin-left:8.33333333%}.ivu-col-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-1{display:block;width:4.16666667%}.ivu-col-push-1{left:4.16666667%}.ivu-col-pull-1{right:4.16666667%}.ivu-col-offset-1{margin-left:4.16666667%}.ivu-col-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-0{display:none}.ivu-col-push-0{left:auto}.ivu-col-pull-0{right:auto}.ivu-col-span-xs-1,.ivu-col-span-xs-10,.ivu-col-span-xs-11,.ivu-col-span-xs-12,.ivu-col-span-xs-13,.ivu-col-span-xs-14,.ivu-col-span-xs-15,.ivu-col-span-xs-16,.ivu-col-span-xs-17,.ivu-col-span-xs-18,.ivu-col-span-xs-19,.ivu-col-span-xs-2,.ivu-col-span-xs-20,.ivu-col-span-xs-21,.ivu-col-span-xs-22,.ivu-col-span-xs-23,.ivu-col-span-xs-24,.ivu-col-span-xs-3,.ivu-col-span-xs-4,.ivu-col-span-xs-5,.ivu-col-span-xs-6,.ivu-col-span-xs-7,.ivu-col-span-xs-8,.ivu-col-span-xs-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-xs-24{display:block;width:100%}.ivu-col-xs-push-24{left:100%}.ivu-col-xs-pull-24{right:100%}.ivu-col-xs-offset-24{margin-left:100%}.ivu-col-xs-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-xs-23{display:block;width:95.83333333%}.ivu-col-xs-push-23{left:95.83333333%}.ivu-col-xs-pull-23{right:95.83333333%}.ivu-col-xs-offset-23{margin-left:95.83333333%}.ivu-col-xs-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-xs-22{display:block;width:91.66666667%}.ivu-col-xs-push-22{left:91.66666667%}.ivu-col-xs-pull-22{right:91.66666667%}.ivu-col-xs-offset-22{margin-left:91.66666667%}.ivu-col-xs-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-xs-21{display:block;width:87.5%}.ivu-col-xs-push-21{left:87.5%}.ivu-col-xs-pull-21{right:87.5%}.ivu-col-xs-offset-21{margin-left:87.5%}.ivu-col-xs-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-xs-20{display:block;width:83.33333333%}.ivu-col-xs-push-20{left:83.33333333%}.ivu-col-xs-pull-20{right:83.33333333%}.ivu-col-xs-offset-20{margin-left:83.33333333%}.ivu-col-xs-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-xs-19{display:block;width:79.16666667%}.ivu-col-xs-push-19{left:79.16666667%}.ivu-col-xs-pull-19{right:79.16666667%}.ivu-col-xs-offset-19{margin-left:79.16666667%}.ivu-col-xs-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-xs-18{display:block;width:75%}.ivu-col-xs-push-18{left:75%}.ivu-col-xs-pull-18{right:75%}.ivu-col-xs-offset-18{margin-left:75%}.ivu-col-xs-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-xs-17{display:block;width:70.83333333%}.ivu-col-xs-push-17{left:70.83333333%}.ivu-col-xs-pull-17{right:70.83333333%}.ivu-col-xs-offset-17{margin-left:70.83333333%}.ivu-col-xs-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-xs-16{display:block;width:66.66666667%}.ivu-col-xs-push-16{left:66.66666667%}.ivu-col-xs-pull-16{right:66.66666667%}.ivu-col-xs-offset-16{margin-left:66.66666667%}.ivu-col-xs-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-xs-15{display:block;width:62.5%}.ivu-col-xs-push-15{left:62.5%}.ivu-col-xs-pull-15{right:62.5%}.ivu-col-xs-offset-15{margin-left:62.5%}.ivu-col-xs-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-xs-14{display:block;width:58.33333333%}.ivu-col-xs-push-14{left:58.33333333%}.ivu-col-xs-pull-14{right:58.33333333%}.ivu-col-xs-offset-14{margin-left:58.33333333%}.ivu-col-xs-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-xs-13{display:block;width:54.16666667%}.ivu-col-xs-push-13{left:54.16666667%}.ivu-col-xs-pull-13{right:54.16666667%}.ivu-col-xs-offset-13{margin-left:54.16666667%}.ivu-col-xs-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-xs-12{display:block;width:50%}.ivu-col-xs-push-12{left:50%}.ivu-col-xs-pull-12{right:50%}.ivu-col-xs-offset-12{margin-left:50%}.ivu-col-xs-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-xs-11{display:block;width:45.83333333%}.ivu-col-xs-push-11{left:45.83333333%}.ivu-col-xs-pull-11{right:45.83333333%}.ivu-col-xs-offset-11{margin-left:45.83333333%}.ivu-col-xs-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-xs-10{display:block;width:41.66666667%}.ivu-col-xs-push-10{left:41.66666667%}.ivu-col-xs-pull-10{right:41.66666667%}.ivu-col-xs-offset-10{margin-left:41.66666667%}.ivu-col-xs-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-xs-9{display:block;width:37.5%}.ivu-col-xs-push-9{left:37.5%}.ivu-col-xs-pull-9{right:37.5%}.ivu-col-xs-offset-9{margin-left:37.5%}.ivu-col-xs-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-xs-8{display:block;width:33.33333333%}.ivu-col-xs-push-8{left:33.33333333%}.ivu-col-xs-pull-8{right:33.33333333%}.ivu-col-xs-offset-8{margin-left:33.33333333%}.ivu-col-xs-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-xs-7{display:block;width:29.16666667%}.ivu-col-xs-push-7{left:29.16666667%}.ivu-col-xs-pull-7{right:29.16666667%}.ivu-col-xs-offset-7{margin-left:29.16666667%}.ivu-col-xs-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-xs-6{display:block;width:25%}.ivu-col-xs-push-6{left:25%}.ivu-col-xs-pull-6{right:25%}.ivu-col-xs-offset-6{margin-left:25%}.ivu-col-xs-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-xs-5{display:block;width:20.83333333%}.ivu-col-xs-push-5{left:20.83333333%}.ivu-col-xs-pull-5{right:20.83333333%}.ivu-col-xs-offset-5{margin-left:20.83333333%}.ivu-col-xs-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-xs-4{display:block;width:16.66666667%}.ivu-col-xs-push-4{left:16.66666667%}.ivu-col-xs-pull-4{right:16.66666667%}.ivu-col-xs-offset-4{margin-left:16.66666667%}.ivu-col-xs-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-xs-3{display:block;width:12.5%}.ivu-col-xs-push-3{left:12.5%}.ivu-col-xs-pull-3{right:12.5%}.ivu-col-xs-offset-3{margin-left:12.5%}.ivu-col-xs-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-xs-2{display:block;width:8.33333333%}.ivu-col-xs-push-2{left:8.33333333%}.ivu-col-xs-pull-2{right:8.33333333%}.ivu-col-xs-offset-2{margin-left:8.33333333%}.ivu-col-xs-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-xs-1{display:block;width:4.16666667%}.ivu-col-xs-push-1{left:4.16666667%}.ivu-col-xs-pull-1{right:4.16666667%}.ivu-col-xs-offset-1{margin-left:4.16666667%}.ivu-col-xs-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-xs-0{display:none}.ivu-col-xs-push-0{left:auto}.ivu-col-xs-pull-0{right:auto}@media (min-width:576px){.ivu-col-span-sm-1,.ivu-col-span-sm-10,.ivu-col-span-sm-11,.ivu-col-span-sm-12,.ivu-col-span-sm-13,.ivu-col-span-sm-14,.ivu-col-span-sm-15,.ivu-col-span-sm-16,.ivu-col-span-sm-17,.ivu-col-span-sm-18,.ivu-col-span-sm-19,.ivu-col-span-sm-2,.ivu-col-span-sm-20,.ivu-col-span-sm-21,.ivu-col-span-sm-22,.ivu-col-span-sm-23,.ivu-col-span-sm-24,.ivu-col-span-sm-3,.ivu-col-span-sm-4,.ivu-col-span-sm-5,.ivu-col-span-sm-6,.ivu-col-span-sm-7,.ivu-col-span-sm-8,.ivu-col-span-sm-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-sm-24{display:block;width:100%}.ivu-col-sm-push-24{left:100%}.ivu-col-sm-pull-24{right:100%}.ivu-col-sm-offset-24{margin-left:100%}.ivu-col-sm-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-sm-23{display:block;width:95.83333333%}.ivu-col-sm-push-23{left:95.83333333%}.ivu-col-sm-pull-23{right:95.83333333%}.ivu-col-sm-offset-23{margin-left:95.83333333%}.ivu-col-sm-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-sm-22{display:block;width:91.66666667%}.ivu-col-sm-push-22{left:91.66666667%}.ivu-col-sm-pull-22{right:91.66666667%}.ivu-col-sm-offset-22{margin-left:91.66666667%}.ivu-col-sm-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-sm-21{display:block;width:87.5%}.ivu-col-sm-push-21{left:87.5%}.ivu-col-sm-pull-21{right:87.5%}.ivu-col-sm-offset-21{margin-left:87.5%}.ivu-col-sm-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-sm-20{display:block;width:83.33333333%}.ivu-col-sm-push-20{left:83.33333333%}.ivu-col-sm-pull-20{right:83.33333333%}.ivu-col-sm-offset-20{margin-left:83.33333333%}.ivu-col-sm-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-sm-19{display:block;width:79.16666667%}.ivu-col-sm-push-19{left:79.16666667%}.ivu-col-sm-pull-19{right:79.16666667%}.ivu-col-sm-offset-19{margin-left:79.16666667%}.ivu-col-sm-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-sm-18{display:block;width:75%}.ivu-col-sm-push-18{left:75%}.ivu-col-sm-pull-18{right:75%}.ivu-col-sm-offset-18{margin-left:75%}.ivu-col-sm-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-sm-17{display:block;width:70.83333333%}.ivu-col-sm-push-17{left:70.83333333%}.ivu-col-sm-pull-17{right:70.83333333%}.ivu-col-sm-offset-17{margin-left:70.83333333%}.ivu-col-sm-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-sm-16{display:block;width:66.66666667%}.ivu-col-sm-push-16{left:66.66666667%}.ivu-col-sm-pull-16{right:66.66666667%}.ivu-col-sm-offset-16{margin-left:66.66666667%}.ivu-col-sm-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-sm-15{display:block;width:62.5%}.ivu-col-sm-push-15{left:62.5%}.ivu-col-sm-pull-15{right:62.5%}.ivu-col-sm-offset-15{margin-left:62.5%}.ivu-col-sm-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-sm-14{display:block;width:58.33333333%}.ivu-col-sm-push-14{left:58.33333333%}.ivu-col-sm-pull-14{right:58.33333333%}.ivu-col-sm-offset-14{margin-left:58.33333333%}.ivu-col-sm-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-sm-13{display:block;width:54.16666667%}.ivu-col-sm-push-13{left:54.16666667%}.ivu-col-sm-pull-13{right:54.16666667%}.ivu-col-sm-offset-13{margin-left:54.16666667%}.ivu-col-sm-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-sm-12{display:block;width:50%}.ivu-col-sm-push-12{left:50%}.ivu-col-sm-pull-12{right:50%}.ivu-col-sm-offset-12{margin-left:50%}.ivu-col-sm-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-sm-11{display:block;width:45.83333333%}.ivu-col-sm-push-11{left:45.83333333%}.ivu-col-sm-pull-11{right:45.83333333%}.ivu-col-sm-offset-11{margin-left:45.83333333%}.ivu-col-sm-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-sm-10{display:block;width:41.66666667%}.ivu-col-sm-push-10{left:41.66666667%}.ivu-col-sm-pull-10{right:41.66666667%}.ivu-col-sm-offset-10{margin-left:41.66666667%}.ivu-col-sm-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-sm-9{display:block;width:37.5%}.ivu-col-sm-push-9{left:37.5%}.ivu-col-sm-pull-9{right:37.5%}.ivu-col-sm-offset-9{margin-left:37.5%}.ivu-col-sm-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-sm-8{display:block;width:33.33333333%}.ivu-col-sm-push-8{left:33.33333333%}.ivu-col-sm-pull-8{right:33.33333333%}.ivu-col-sm-offset-8{margin-left:33.33333333%}.ivu-col-sm-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-sm-7{display:block;width:29.16666667%}.ivu-col-sm-push-7{left:29.16666667%}.ivu-col-sm-pull-7{right:29.16666667%}.ivu-col-sm-offset-7{margin-left:29.16666667%}.ivu-col-sm-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-sm-6{display:block;width:25%}.ivu-col-sm-push-6{left:25%}.ivu-col-sm-pull-6{right:25%}.ivu-col-sm-offset-6{margin-left:25%}.ivu-col-sm-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-sm-5{display:block;width:20.83333333%}.ivu-col-sm-push-5{left:20.83333333%}.ivu-col-sm-pull-5{right:20.83333333%}.ivu-col-sm-offset-5{margin-left:20.83333333%}.ivu-col-sm-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-sm-4{display:block;width:16.66666667%}.ivu-col-sm-push-4{left:16.66666667%}.ivu-col-sm-pull-4{right:16.66666667%}.ivu-col-sm-offset-4{margin-left:16.66666667%}.ivu-col-sm-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-sm-3{display:block;width:12.5%}.ivu-col-sm-push-3{left:12.5%}.ivu-col-sm-pull-3{right:12.5%}.ivu-col-sm-offset-3{margin-left:12.5%}.ivu-col-sm-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-sm-2{display:block;width:8.33333333%}.ivu-col-sm-push-2{left:8.33333333%}.ivu-col-sm-pull-2{right:8.33333333%}.ivu-col-sm-offset-2{margin-left:8.33333333%}.ivu-col-sm-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-sm-1{display:block;width:4.16666667%}.ivu-col-sm-push-1{left:4.16666667%}.ivu-col-sm-pull-1{right:4.16666667%}.ivu-col-sm-offset-1{margin-left:4.16666667%}.ivu-col-sm-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-sm-0{display:none}.ivu-col-sm-push-0{left:auto}.ivu-col-sm-pull-0{right:auto}}@media (min-width:768px){.ivu-col-span-md-1,.ivu-col-span-md-10,.ivu-col-span-md-11,.ivu-col-span-md-12,.ivu-col-span-md-13,.ivu-col-span-md-14,.ivu-col-span-md-15,.ivu-col-span-md-16,.ivu-col-span-md-17,.ivu-col-span-md-18,.ivu-col-span-md-19,.ivu-col-span-md-2,.ivu-col-span-md-20,.ivu-col-span-md-21,.ivu-col-span-md-22,.ivu-col-span-md-23,.ivu-col-span-md-24,.ivu-col-span-md-3,.ivu-col-span-md-4,.ivu-col-span-md-5,.ivu-col-span-md-6,.ivu-col-span-md-7,.ivu-col-span-md-8,.ivu-col-span-md-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-md-24{display:block;width:100%}.ivu-col-md-push-24{left:100%}.ivu-col-md-pull-24{right:100%}.ivu-col-md-offset-24{margin-left:100%}.ivu-col-md-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-md-23{display:block;width:95.83333333%}.ivu-col-md-push-23{left:95.83333333%}.ivu-col-md-pull-23{right:95.83333333%}.ivu-col-md-offset-23{margin-left:95.83333333%}.ivu-col-md-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-md-22{display:block;width:91.66666667%}.ivu-col-md-push-22{left:91.66666667%}.ivu-col-md-pull-22{right:91.66666667%}.ivu-col-md-offset-22{margin-left:91.66666667%}.ivu-col-md-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-md-21{display:block;width:87.5%}.ivu-col-md-push-21{left:87.5%}.ivu-col-md-pull-21{right:87.5%}.ivu-col-md-offset-21{margin-left:87.5%}.ivu-col-md-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-md-20{display:block;width:83.33333333%}.ivu-col-md-push-20{left:83.33333333%}.ivu-col-md-pull-20{right:83.33333333%}.ivu-col-md-offset-20{margin-left:83.33333333%}.ivu-col-md-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-md-19{display:block;width:79.16666667%}.ivu-col-md-push-19{left:79.16666667%}.ivu-col-md-pull-19{right:79.16666667%}.ivu-col-md-offset-19{margin-left:79.16666667%}.ivu-col-md-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-md-18{display:block;width:75%}.ivu-col-md-push-18{left:75%}.ivu-col-md-pull-18{right:75%}.ivu-col-md-offset-18{margin-left:75%}.ivu-col-md-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-md-17{display:block;width:70.83333333%}.ivu-col-md-push-17{left:70.83333333%}.ivu-col-md-pull-17{right:70.83333333%}.ivu-col-md-offset-17{margin-left:70.83333333%}.ivu-col-md-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-md-16{display:block;width:66.66666667%}.ivu-col-md-push-16{left:66.66666667%}.ivu-col-md-pull-16{right:66.66666667%}.ivu-col-md-offset-16{margin-left:66.66666667%}.ivu-col-md-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-md-15{display:block;width:62.5%}.ivu-col-md-push-15{left:62.5%}.ivu-col-md-pull-15{right:62.5%}.ivu-col-md-offset-15{margin-left:62.5%}.ivu-col-md-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-md-14{display:block;width:58.33333333%}.ivu-col-md-push-14{left:58.33333333%}.ivu-col-md-pull-14{right:58.33333333%}.ivu-col-md-offset-14{margin-left:58.33333333%}.ivu-col-md-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-md-13{display:block;width:54.16666667%}.ivu-col-md-push-13{left:54.16666667%}.ivu-col-md-pull-13{right:54.16666667%}.ivu-col-md-offset-13{margin-left:54.16666667%}.ivu-col-md-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-md-12{display:block;width:50%}.ivu-col-md-push-12{left:50%}.ivu-col-md-pull-12{right:50%}.ivu-col-md-offset-12{margin-left:50%}.ivu-col-md-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-md-11{display:block;width:45.83333333%}.ivu-col-md-push-11{left:45.83333333%}.ivu-col-md-pull-11{right:45.83333333%}.ivu-col-md-offset-11{margin-left:45.83333333%}.ivu-col-md-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-md-10{display:block;width:41.66666667%}.ivu-col-md-push-10{left:41.66666667%}.ivu-col-md-pull-10{right:41.66666667%}.ivu-col-md-offset-10{margin-left:41.66666667%}.ivu-col-md-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-md-9{display:block;width:37.5%}.ivu-col-md-push-9{left:37.5%}.ivu-col-md-pull-9{right:37.5%}.ivu-col-md-offset-9{margin-left:37.5%}.ivu-col-md-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-md-8{display:block;width:33.33333333%}.ivu-col-md-push-8{left:33.33333333%}.ivu-col-md-pull-8{right:33.33333333%}.ivu-col-md-offset-8{margin-left:33.33333333%}.ivu-col-md-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-md-7{display:block;width:29.16666667%}.ivu-col-md-push-7{left:29.16666667%}.ivu-col-md-pull-7{right:29.16666667%}.ivu-col-md-offset-7{margin-left:29.16666667%}.ivu-col-md-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-md-6{display:block;width:25%}.ivu-col-md-push-6{left:25%}.ivu-col-md-pull-6{right:25%}.ivu-col-md-offset-6{margin-left:25%}.ivu-col-md-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-md-5{display:block;width:20.83333333%}.ivu-col-md-push-5{left:20.83333333%}.ivu-col-md-pull-5{right:20.83333333%}.ivu-col-md-offset-5{margin-left:20.83333333%}.ivu-col-md-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-md-4{display:block;width:16.66666667%}.ivu-col-md-push-4{left:16.66666667%}.ivu-col-md-pull-4{right:16.66666667%}.ivu-col-md-offset-4{margin-left:16.66666667%}.ivu-col-md-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-md-3{display:block;width:12.5%}.ivu-col-md-push-3{left:12.5%}.ivu-col-md-pull-3{right:12.5%}.ivu-col-md-offset-3{margin-left:12.5%}.ivu-col-md-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-md-2{display:block;width:8.33333333%}.ivu-col-md-push-2{left:8.33333333%}.ivu-col-md-pull-2{right:8.33333333%}.ivu-col-md-offset-2{margin-left:8.33333333%}.ivu-col-md-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-md-1{display:block;width:4.16666667%}.ivu-col-md-push-1{left:4.16666667%}.ivu-col-md-pull-1{right:4.16666667%}.ivu-col-md-offset-1{margin-left:4.16666667%}.ivu-col-md-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-md-0{display:none}.ivu-col-md-push-0{left:auto}.ivu-col-md-pull-0{right:auto}}@media (min-width:992px){.ivu-col-span-lg-1,.ivu-col-span-lg-10,.ivu-col-span-lg-11,.ivu-col-span-lg-12,.ivu-col-span-lg-13,.ivu-col-span-lg-14,.ivu-col-span-lg-15,.ivu-col-span-lg-16,.ivu-col-span-lg-17,.ivu-col-span-lg-18,.ivu-col-span-lg-19,.ivu-col-span-lg-2,.ivu-col-span-lg-20,.ivu-col-span-lg-21,.ivu-col-span-lg-22,.ivu-col-span-lg-23,.ivu-col-span-lg-24,.ivu-col-span-lg-3,.ivu-col-span-lg-4,.ivu-col-span-lg-5,.ivu-col-span-lg-6,.ivu-col-span-lg-7,.ivu-col-span-lg-8,.ivu-col-span-lg-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-lg-24{display:block;width:100%}.ivu-col-lg-push-24{left:100%}.ivu-col-lg-pull-24{right:100%}.ivu-col-lg-offset-24{margin-left:100%}.ivu-col-lg-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-lg-23{display:block;width:95.83333333%}.ivu-col-lg-push-23{left:95.83333333%}.ivu-col-lg-pull-23{right:95.83333333%}.ivu-col-lg-offset-23{margin-left:95.83333333%}.ivu-col-lg-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-lg-22{display:block;width:91.66666667%}.ivu-col-lg-push-22{left:91.66666667%}.ivu-col-lg-pull-22{right:91.66666667%}.ivu-col-lg-offset-22{margin-left:91.66666667%}.ivu-col-lg-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-lg-21{display:block;width:87.5%}.ivu-col-lg-push-21{left:87.5%}.ivu-col-lg-pull-21{right:87.5%}.ivu-col-lg-offset-21{margin-left:87.5%}.ivu-col-lg-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-lg-20{display:block;width:83.33333333%}.ivu-col-lg-push-20{left:83.33333333%}.ivu-col-lg-pull-20{right:83.33333333%}.ivu-col-lg-offset-20{margin-left:83.33333333%}.ivu-col-lg-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-lg-19{display:block;width:79.16666667%}.ivu-col-lg-push-19{left:79.16666667%}.ivu-col-lg-pull-19{right:79.16666667%}.ivu-col-lg-offset-19{margin-left:79.16666667%}.ivu-col-lg-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-lg-18{display:block;width:75%}.ivu-col-lg-push-18{left:75%}.ivu-col-lg-pull-18{right:75%}.ivu-col-lg-offset-18{margin-left:75%}.ivu-col-lg-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-lg-17{display:block;width:70.83333333%}.ivu-col-lg-push-17{left:70.83333333%}.ivu-col-lg-pull-17{right:70.83333333%}.ivu-col-lg-offset-17{margin-left:70.83333333%}.ivu-col-lg-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-lg-16{display:block;width:66.66666667%}.ivu-col-lg-push-16{left:66.66666667%}.ivu-col-lg-pull-16{right:66.66666667%}.ivu-col-lg-offset-16{margin-left:66.66666667%}.ivu-col-lg-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-lg-15{display:block;width:62.5%}.ivu-col-lg-push-15{left:62.5%}.ivu-col-lg-pull-15{right:62.5%}.ivu-col-lg-offset-15{margin-left:62.5%}.ivu-col-lg-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-lg-14{display:block;width:58.33333333%}.ivu-col-lg-push-14{left:58.33333333%}.ivu-col-lg-pull-14{right:58.33333333%}.ivu-col-lg-offset-14{margin-left:58.33333333%}.ivu-col-lg-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-lg-13{display:block;width:54.16666667%}.ivu-col-lg-push-13{left:54.16666667%}.ivu-col-lg-pull-13{right:54.16666667%}.ivu-col-lg-offset-13{margin-left:54.16666667%}.ivu-col-lg-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-lg-12{display:block;width:50%}.ivu-col-lg-push-12{left:50%}.ivu-col-lg-pull-12{right:50%}.ivu-col-lg-offset-12{margin-left:50%}.ivu-col-lg-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-lg-11{display:block;width:45.83333333%}.ivu-col-lg-push-11{left:45.83333333%}.ivu-col-lg-pull-11{right:45.83333333%}.ivu-col-lg-offset-11{margin-left:45.83333333%}.ivu-col-lg-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-lg-10{display:block;width:41.66666667%}.ivu-col-lg-push-10{left:41.66666667%}.ivu-col-lg-pull-10{right:41.66666667%}.ivu-col-lg-offset-10{margin-left:41.66666667%}.ivu-col-lg-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-lg-9{display:block;width:37.5%}.ivu-col-lg-push-9{left:37.5%}.ivu-col-lg-pull-9{right:37.5%}.ivu-col-lg-offset-9{margin-left:37.5%}.ivu-col-lg-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-lg-8{display:block;width:33.33333333%}.ivu-col-lg-push-8{left:33.33333333%}.ivu-col-lg-pull-8{right:33.33333333%}.ivu-col-lg-offset-8{margin-left:33.33333333%}.ivu-col-lg-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-lg-7{display:block;width:29.16666667%}.ivu-col-lg-push-7{left:29.16666667%}.ivu-col-lg-pull-7{right:29.16666667%}.ivu-col-lg-offset-7{margin-left:29.16666667%}.ivu-col-lg-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-lg-6{display:block;width:25%}.ivu-col-lg-push-6{left:25%}.ivu-col-lg-pull-6{right:25%}.ivu-col-lg-offset-6{margin-left:25%}.ivu-col-lg-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-lg-5{display:block;width:20.83333333%}.ivu-col-lg-push-5{left:20.83333333%}.ivu-col-lg-pull-5{right:20.83333333%}.ivu-col-lg-offset-5{margin-left:20.83333333%}.ivu-col-lg-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-lg-4{display:block;width:16.66666667%}.ivu-col-lg-push-4{left:16.66666667%}.ivu-col-lg-pull-4{right:16.66666667%}.ivu-col-lg-offset-4{margin-left:16.66666667%}.ivu-col-lg-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-lg-3{display:block;width:12.5%}.ivu-col-lg-push-3{left:12.5%}.ivu-col-lg-pull-3{right:12.5%}.ivu-col-lg-offset-3{margin-left:12.5%}.ivu-col-lg-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-lg-2{display:block;width:8.33333333%}.ivu-col-lg-push-2{left:8.33333333%}.ivu-col-lg-pull-2{right:8.33333333%}.ivu-col-lg-offset-2{margin-left:8.33333333%}.ivu-col-lg-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-lg-1{display:block;width:4.16666667%}.ivu-col-lg-push-1{left:4.16666667%}.ivu-col-lg-pull-1{right:4.16666667%}.ivu-col-lg-offset-1{margin-left:4.16666667%}.ivu-col-lg-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-lg-0{display:none}.ivu-col-lg-push-0{left:auto}.ivu-col-lg-pull-0{right:auto}}@media (min-width:1200px){.ivu-col-span-xl-1,.ivu-col-span-xl-10,.ivu-col-span-xl-11,.ivu-col-span-xl-12,.ivu-col-span-xl-13,.ivu-col-span-xl-14,.ivu-col-span-xl-15,.ivu-col-span-xl-16,.ivu-col-span-xl-17,.ivu-col-span-xl-18,.ivu-col-span-xl-19,.ivu-col-span-xl-2,.ivu-col-span-xl-20,.ivu-col-span-xl-21,.ivu-col-span-xl-22,.ivu-col-span-xl-23,.ivu-col-span-xl-24,.ivu-col-span-xl-3,.ivu-col-span-xl-4,.ivu-col-span-xl-5,.ivu-col-span-xl-6,.ivu-col-span-xl-7,.ivu-col-span-xl-8,.ivu-col-span-xl-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-xl-24{display:block;width:100%}.ivu-col-xl-push-24{left:100%}.ivu-col-xl-pull-24{right:100%}.ivu-col-xl-offset-24{margin-left:100%}.ivu-col-xl-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-xl-23{display:block;width:95.83333333%}.ivu-col-xl-push-23{left:95.83333333%}.ivu-col-xl-pull-23{right:95.83333333%}.ivu-col-xl-offset-23{margin-left:95.83333333%}.ivu-col-xl-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-xl-22{display:block;width:91.66666667%}.ivu-col-xl-push-22{left:91.66666667%}.ivu-col-xl-pull-22{right:91.66666667%}.ivu-col-xl-offset-22{margin-left:91.66666667%}.ivu-col-xl-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-xl-21{display:block;width:87.5%}.ivu-col-xl-push-21{left:87.5%}.ivu-col-xl-pull-21{right:87.5%}.ivu-col-xl-offset-21{margin-left:87.5%}.ivu-col-xl-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-xl-20{display:block;width:83.33333333%}.ivu-col-xl-push-20{left:83.33333333%}.ivu-col-xl-pull-20{right:83.33333333%}.ivu-col-xl-offset-20{margin-left:83.33333333%}.ivu-col-xl-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-xl-19{display:block;width:79.16666667%}.ivu-col-xl-push-19{left:79.16666667%}.ivu-col-xl-pull-19{right:79.16666667%}.ivu-col-xl-offset-19{margin-left:79.16666667%}.ivu-col-xl-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-xl-18{display:block;width:75%}.ivu-col-xl-push-18{left:75%}.ivu-col-xl-pull-18{right:75%}.ivu-col-xl-offset-18{margin-left:75%}.ivu-col-xl-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-xl-17{display:block;width:70.83333333%}.ivu-col-xl-push-17{left:70.83333333%}.ivu-col-xl-pull-17{right:70.83333333%}.ivu-col-xl-offset-17{margin-left:70.83333333%}.ivu-col-xl-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-xl-16{display:block;width:66.66666667%}.ivu-col-xl-push-16{left:66.66666667%}.ivu-col-xl-pull-16{right:66.66666667%}.ivu-col-xl-offset-16{margin-left:66.66666667%}.ivu-col-xl-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-xl-15{display:block;width:62.5%}.ivu-col-xl-push-15{left:62.5%}.ivu-col-xl-pull-15{right:62.5%}.ivu-col-xl-offset-15{margin-left:62.5%}.ivu-col-xl-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-xl-14{display:block;width:58.33333333%}.ivu-col-xl-push-14{left:58.33333333%}.ivu-col-xl-pull-14{right:58.33333333%}.ivu-col-xl-offset-14{margin-left:58.33333333%}.ivu-col-xl-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-xl-13{display:block;width:54.16666667%}.ivu-col-xl-push-13{left:54.16666667%}.ivu-col-xl-pull-13{right:54.16666667%}.ivu-col-xl-offset-13{margin-left:54.16666667%}.ivu-col-xl-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-xl-12{display:block;width:50%}.ivu-col-xl-push-12{left:50%}.ivu-col-xl-pull-12{right:50%}.ivu-col-xl-offset-12{margin-left:50%}.ivu-col-xl-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-xl-11{display:block;width:45.83333333%}.ivu-col-xl-push-11{left:45.83333333%}.ivu-col-xl-pull-11{right:45.83333333%}.ivu-col-xl-offset-11{margin-left:45.83333333%}.ivu-col-xl-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-xl-10{display:block;width:41.66666667%}.ivu-col-xl-push-10{left:41.66666667%}.ivu-col-xl-pull-10{right:41.66666667%}.ivu-col-xl-offset-10{margin-left:41.66666667%}.ivu-col-xl-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-xl-9{display:block;width:37.5%}.ivu-col-xl-push-9{left:37.5%}.ivu-col-xl-pull-9{right:37.5%}.ivu-col-xl-offset-9{margin-left:37.5%}.ivu-col-xl-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-xl-8{display:block;width:33.33333333%}.ivu-col-xl-push-8{left:33.33333333%}.ivu-col-xl-pull-8{right:33.33333333%}.ivu-col-xl-offset-8{margin-left:33.33333333%}.ivu-col-xl-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-xl-7{display:block;width:29.16666667%}.ivu-col-xl-push-7{left:29.16666667%}.ivu-col-xl-pull-7{right:29.16666667%}.ivu-col-xl-offset-7{margin-left:29.16666667%}.ivu-col-xl-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-xl-6{display:block;width:25%}.ivu-col-xl-push-6{left:25%}.ivu-col-xl-pull-6{right:25%}.ivu-col-xl-offset-6{margin-left:25%}.ivu-col-xl-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-xl-5{display:block;width:20.83333333%}.ivu-col-xl-push-5{left:20.83333333%}.ivu-col-xl-pull-5{right:20.83333333%}.ivu-col-xl-offset-5{margin-left:20.83333333%}.ivu-col-xl-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-xl-4{display:block;width:16.66666667%}.ivu-col-xl-push-4{left:16.66666667%}.ivu-col-xl-pull-4{right:16.66666667%}.ivu-col-xl-offset-4{margin-left:16.66666667%}.ivu-col-xl-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-xl-3{display:block;width:12.5%}.ivu-col-xl-push-3{left:12.5%}.ivu-col-xl-pull-3{right:12.5%}.ivu-col-xl-offset-3{margin-left:12.5%}.ivu-col-xl-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-xl-2{display:block;width:8.33333333%}.ivu-col-xl-push-2{left:8.33333333%}.ivu-col-xl-pull-2{right:8.33333333%}.ivu-col-xl-offset-2{margin-left:8.33333333%}.ivu-col-xl-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-xl-1{display:block;width:4.16666667%}.ivu-col-xl-push-1{left:4.16666667%}.ivu-col-xl-pull-1{right:4.16666667%}.ivu-col-xl-offset-1{margin-left:4.16666667%}.ivu-col-xl-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-xl-0{display:none}.ivu-col-xl-push-0{left:auto}.ivu-col-xl-pull-0{right:auto}}@media (min-width:1600px){.ivu-col-span-xxl-1,.ivu-col-span-xxl-10,.ivu-col-span-xxl-11,.ivu-col-span-xxl-12,.ivu-col-span-xxl-13,.ivu-col-span-xxl-14,.ivu-col-span-xxl-15,.ivu-col-span-xxl-16,.ivu-col-span-xxl-17,.ivu-col-span-xxl-18,.ivu-col-span-xxl-19,.ivu-col-span-xxl-2,.ivu-col-span-xxl-20,.ivu-col-span-xxl-21,.ivu-col-span-xxl-22,.ivu-col-span-xxl-23,.ivu-col-span-xxl-24,.ivu-col-span-xxl-3,.ivu-col-span-xxl-4,.ivu-col-span-xxl-5,.ivu-col-span-xxl-6,.ivu-col-span-xxl-7,.ivu-col-span-xxl-8,.ivu-col-span-xxl-9{float:left;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-col-span-xxl-24{display:block;width:100%}.ivu-col-xxl-push-24{left:100%}.ivu-col-xxl-pull-24{right:100%}.ivu-col-xxl-offset-24{margin-left:100%}.ivu-col-xxl-order-24{-webkit-box-ordinal-group:25;-ms-flex-order:24;order:24}.ivu-col-span-xxl-23{display:block;width:95.83333333%}.ivu-col-xxl-push-23{left:95.83333333%}.ivu-col-xxl-pull-23{right:95.83333333%}.ivu-col-xxl-offset-23{margin-left:95.83333333%}.ivu-col-xxl-order-23{-webkit-box-ordinal-group:24;-ms-flex-order:23;order:23}.ivu-col-span-xxl-22{display:block;width:91.66666667%}.ivu-col-xxl-push-22{left:91.66666667%}.ivu-col-xxl-pull-22{right:91.66666667%}.ivu-col-xxl-offset-22{margin-left:91.66666667%}.ivu-col-xxl-order-22{-webkit-box-ordinal-group:23;-ms-flex-order:22;order:22}.ivu-col-span-xxl-21{display:block;width:87.5%}.ivu-col-xxl-push-21{left:87.5%}.ivu-col-xxl-pull-21{right:87.5%}.ivu-col-xxl-offset-21{margin-left:87.5%}.ivu-col-xxl-order-21{-webkit-box-ordinal-group:22;-ms-flex-order:21;order:21}.ivu-col-span-xxl-20{display:block;width:83.33333333%}.ivu-col-xxl-push-20{left:83.33333333%}.ivu-col-xxl-pull-20{right:83.33333333%}.ivu-col-xxl-offset-20{margin-left:83.33333333%}.ivu-col-xxl-order-20{-webkit-box-ordinal-group:21;-ms-flex-order:20;order:20}.ivu-col-span-xxl-19{display:block;width:79.16666667%}.ivu-col-xxl-push-19{left:79.16666667%}.ivu-col-xxl-pull-19{right:79.16666667%}.ivu-col-xxl-offset-19{margin-left:79.16666667%}.ivu-col-xxl-order-19{-webkit-box-ordinal-group:20;-ms-flex-order:19;order:19}.ivu-col-span-xxl-18{display:block;width:75%}.ivu-col-xxl-push-18{left:75%}.ivu-col-xxl-pull-18{right:75%}.ivu-col-xxl-offset-18{margin-left:75%}.ivu-col-xxl-order-18{-webkit-box-ordinal-group:19;-ms-flex-order:18;order:18}.ivu-col-span-xxl-17{display:block;width:70.83333333%}.ivu-col-xxl-push-17{left:70.83333333%}.ivu-col-xxl-pull-17{right:70.83333333%}.ivu-col-xxl-offset-17{margin-left:70.83333333%}.ivu-col-xxl-order-17{-webkit-box-ordinal-group:18;-ms-flex-order:17;order:17}.ivu-col-span-xxl-16{display:block;width:66.66666667%}.ivu-col-xxl-push-16{left:66.66666667%}.ivu-col-xxl-pull-16{right:66.66666667%}.ivu-col-xxl-offset-16{margin-left:66.66666667%}.ivu-col-xxl-order-16{-webkit-box-ordinal-group:17;-ms-flex-order:16;order:16}.ivu-col-span-xxl-15{display:block;width:62.5%}.ivu-col-xxl-push-15{left:62.5%}.ivu-col-xxl-pull-15{right:62.5%}.ivu-col-xxl-offset-15{margin-left:62.5%}.ivu-col-xxl-order-15{-webkit-box-ordinal-group:16;-ms-flex-order:15;order:15}.ivu-col-span-xxl-14{display:block;width:58.33333333%}.ivu-col-xxl-push-14{left:58.33333333%}.ivu-col-xxl-pull-14{right:58.33333333%}.ivu-col-xxl-offset-14{margin-left:58.33333333%}.ivu-col-xxl-order-14{-webkit-box-ordinal-group:15;-ms-flex-order:14;order:14}.ivu-col-span-xxl-13{display:block;width:54.16666667%}.ivu-col-xxl-push-13{left:54.16666667%}.ivu-col-xxl-pull-13{right:54.16666667%}.ivu-col-xxl-offset-13{margin-left:54.16666667%}.ivu-col-xxl-order-13{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.ivu-col-span-xxl-12{display:block;width:50%}.ivu-col-xxl-push-12{left:50%}.ivu-col-xxl-pull-12{right:50%}.ivu-col-xxl-offset-12{margin-left:50%}.ivu-col-xxl-order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.ivu-col-span-xxl-11{display:block;width:45.83333333%}.ivu-col-xxl-push-11{left:45.83333333%}.ivu-col-xxl-pull-11{right:45.83333333%}.ivu-col-xxl-offset-11{margin-left:45.83333333%}.ivu-col-xxl-order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.ivu-col-span-xxl-10{display:block;width:41.66666667%}.ivu-col-xxl-push-10{left:41.66666667%}.ivu-col-xxl-pull-10{right:41.66666667%}.ivu-col-xxl-offset-10{margin-left:41.66666667%}.ivu-col-xxl-order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.ivu-col-span-xxl-9{display:block;width:37.5%}.ivu-col-xxl-push-9{left:37.5%}.ivu-col-xxl-pull-9{right:37.5%}.ivu-col-xxl-offset-9{margin-left:37.5%}.ivu-col-xxl-order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.ivu-col-span-xxl-8{display:block;width:33.33333333%}.ivu-col-xxl-push-8{left:33.33333333%}.ivu-col-xxl-pull-8{right:33.33333333%}.ivu-col-xxl-offset-8{margin-left:33.33333333%}.ivu-col-xxl-order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.ivu-col-span-xxl-7{display:block;width:29.16666667%}.ivu-col-xxl-push-7{left:29.16666667%}.ivu-col-xxl-pull-7{right:29.16666667%}.ivu-col-xxl-offset-7{margin-left:29.16666667%}.ivu-col-xxl-order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.ivu-col-span-xxl-6{display:block;width:25%}.ivu-col-xxl-push-6{left:25%}.ivu-col-xxl-pull-6{right:25%}.ivu-col-xxl-offset-6{margin-left:25%}.ivu-col-xxl-order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.ivu-col-span-xxl-5{display:block;width:20.83333333%}.ivu-col-xxl-push-5{left:20.83333333%}.ivu-col-xxl-pull-5{right:20.83333333%}.ivu-col-xxl-offset-5{margin-left:20.83333333%}.ivu-col-xxl-order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.ivu-col-span-xxl-4{display:block;width:16.66666667%}.ivu-col-xxl-push-4{left:16.66666667%}.ivu-col-xxl-pull-4{right:16.66666667%}.ivu-col-xxl-offset-4{margin-left:16.66666667%}.ivu-col-xxl-order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.ivu-col-span-xxl-3{display:block;width:12.5%}.ivu-col-xxl-push-3{left:12.5%}.ivu-col-xxl-pull-3{right:12.5%}.ivu-col-xxl-offset-3{margin-left:12.5%}.ivu-col-xxl-order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.ivu-col-span-xxl-2{display:block;width:8.33333333%}.ivu-col-xxl-push-2{left:8.33333333%}.ivu-col-xxl-pull-2{right:8.33333333%}.ivu-col-xxl-offset-2{margin-left:8.33333333%}.ivu-col-xxl-order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.ivu-col-span-xxl-1{display:block;width:4.16666667%}.ivu-col-xxl-push-1{left:4.16666667%}.ivu-col-xxl-pull-1{right:4.16666667%}.ivu-col-xxl-offset-1{margin-left:4.16666667%}.ivu-col-xxl-order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.ivu-col-span-xxl-0{display:none}.ivu-col-xxl-push-0{left:auto}.ivu-col-xxl-pull-0{right:auto}}.ivu-article h1{font-size:26px;font-weight:400}.ivu-article h2{font-size:20px;font-weight:400}.ivu-article h3{font-size:16px;font-weight:400}.ivu-article h4{font-size:14px;font-weight:400}.ivu-article h5{font-size:12px;font-weight:400}.ivu-article h6{font-size:12px;font-weight:400}.ivu-article blockquote{padding:5px 5px 3px 10px;line-height:1.5;border-left:4px solid #ddd;margin-bottom:20px;color:#666;font-size:14px}.ivu-article ul:not([class^=ivu-]){padding-left:40px;list-style-type:disc}.ivu-article li:not([class^=ivu-]){margin-bottom:5px;font-size:14px}.ivu-article ol ul:not([class^=ivu-]),.ivu-article ul ul:not([class^=ivu-]){list-style-type:circle}.ivu-article p{margin:5px;font-size:14px}.ivu-article a:not([class^=ivu-])[target="_blank"]:after{content:"\F3F2";font-family:Ionicons;color:#aaa;margin-left:3px}.fade-appear,.fade-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.fade-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.fade-appear,.fade-enter-active{-webkit-animation-name:ivuFadeIn;animation-name:ivuFadeIn;-webkit-animation-play-state:running;animation-play-state:running}.fade-leave-active{-webkit-animation-name:ivuFadeOut;animation-name:ivuFadeOut;-webkit-animation-play-state:running;animation-play-state:running}.fade-appear,.fade-enter-active{opacity:0;-webkit-animation-timing-function:linear;animation-timing-function:linear}.fade-leave-active{-webkit-animation-timing-function:linear;animation-timing-function:linear}@-webkit-keyframes ivuFadeIn{0%{opacity:0}100%{opacity:1}}@keyframes ivuFadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes ivuFadeOut{0%{opacity:1}100%{opacity:0}}@keyframes ivuFadeOut{0%{opacity:1}100%{opacity:0}}.move-up-appear,.move-up-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-up-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-up-appear,.move-up-enter-active{-webkit-animation-name:ivuMoveUpIn;animation-name:ivuMoveUpIn;-webkit-animation-play-state:running;animation-play-state:running}.move-up-leave-active{-webkit-animation-name:ivuMoveUpOut;animation-name:ivuMoveUpOut;-webkit-animation-play-state:running;animation-play-state:running}.move-up-appear,.move-up-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-up-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-down-appear,.move-down-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-down-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-down-appear,.move-down-enter-active{-webkit-animation-name:ivuMoveDownIn;animation-name:ivuMoveDownIn;-webkit-animation-play-state:running;animation-play-state:running}.move-down-leave-active{-webkit-animation-name:ivuMoveDownOut;animation-name:ivuMoveDownOut;-webkit-animation-play-state:running;animation-play-state:running}.move-down-appear,.move-down-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-down-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-left-appear,.move-left-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-left-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-left-appear,.move-left-enter-active{-webkit-animation-name:ivuMoveLeftIn;animation-name:ivuMoveLeftIn;-webkit-animation-play-state:running;animation-play-state:running}.move-left-leave-active{-webkit-animation-name:ivuMoveLeftOut;animation-name:ivuMoveLeftOut;-webkit-animation-play-state:running;animation-play-state:running}.move-left-appear,.move-left-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-left-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-right-appear,.move-right-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-right-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-right-appear,.move-right-enter-active{-webkit-animation-name:ivuMoveRightIn;animation-name:ivuMoveRightIn;-webkit-animation-play-state:running;animation-play-state:running}.move-right-leave-active{-webkit-animation-name:ivuMoveRightOut;animation-name:ivuMoveRightOut;-webkit-animation-play-state:running;animation-play-state:running}.move-right-appear,.move-right-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-right-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes ivuMoveDownIn{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes ivuMoveDownIn{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@-webkit-keyframes ivuMoveDownOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes ivuMoveDownOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@-webkit-keyframes ivuMoveLeftIn{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}}@keyframes ivuMoveLeftIn{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}}@-webkit-keyframes ivuMoveLeftOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}@keyframes ivuMoveLeftOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}@-webkit-keyframes ivuMoveRightIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes ivuMoveRightIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes ivuMoveRightOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@keyframes ivuMoveRightOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@-webkit-keyframes ivuMoveUpIn{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(-100%);transform:translateY(-100%);opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes ivuMoveUpIn{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(-100%);transform:translateY(-100%);opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@-webkit-keyframes ivuMoveUpOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(-100%);transform:translateY(-100%);opacity:0}}@keyframes ivuMoveUpOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(0);transform:translateY(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateY(-100%);transform:translateY(-100%);opacity:0}}.move-notice-appear,.move-notice-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-notice-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-notice-appear,.move-notice-enter-active{-webkit-animation-name:ivuMoveNoticeIn;animation-name:ivuMoveNoticeIn;-webkit-animation-play-state:running;animation-play-state:running}.move-notice-leave-active{-webkit-animation-name:ivuMoveNoticeOut;animation-name:ivuMoveNoticeOut;-webkit-animation-play-state:running;animation-play-state:running}.move-notice-appear,.move-notice-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.move-notice-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes ivuMoveNoticeIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes ivuMoveNoticeIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes ivuMoveNoticeOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}70%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);height:auto;padding:16px;margin-bottom:10px;opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);height:0;padding:0;margin-bottom:0;opacity:0}}@keyframes ivuMoveNoticeOut{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}70%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);height:auto;padding:16px;margin-bottom:10px;opacity:0}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);height:0;padding:0;margin-bottom:0;opacity:0}}.ease-appear,.ease-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.ease-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.ease-appear,.ease-enter-active{-webkit-animation-name:ivuEaseIn;animation-name:ivuEaseIn;-webkit-animation-play-state:running;animation-play-state:running}.ease-leave-active{-webkit-animation-name:ivuEaseOut;animation-name:ivuEaseOut;-webkit-animation-play-state:running;animation-play-state:running}.ease-appear,.ease-enter-active{opacity:0;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-duration:.2s;animation-duration:.2s}.ease-leave-active{-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-duration:.2s;animation-duration:.2s}@-webkit-keyframes ivuEaseIn{0%{opacity:0;-webkit-transform:scale(.9);transform:scale(.9)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes ivuEaseIn{0%{opacity:0;-webkit-transform:scale(.9);transform:scale(.9)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes ivuEaseOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.9);transform:scale(.9)}}@keyframes ivuEaseOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.9);transform:scale(.9)}}.transition-drop-appear,.transition-drop-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.transition-drop-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.transition-drop-appear,.transition-drop-enter-active{-webkit-animation-name:ivuTransitionDropIn;animation-name:ivuTransitionDropIn;-webkit-animation-play-state:running;animation-play-state:running}.transition-drop-leave-active{-webkit-animation-name:ivuTransitionDropOut;animation-name:ivuTransitionDropOut;-webkit-animation-play-state:running;animation-play-state:running}.transition-drop-appear,.transition-drop-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.transition-drop-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-up-appear,.slide-up-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-up-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-up-appear,.slide-up-enter-active{-webkit-animation-name:ivuSlideUpIn;animation-name:ivuSlideUpIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-up-leave-active{-webkit-animation-name:ivuSlideUpOut;animation-name:ivuSlideUpOut;-webkit-animation-play-state:running;animation-play-state:running}.slide-up-appear,.slide-up-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-up-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-down-appear,.slide-down-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-down-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-down-appear,.slide-down-enter-active{-webkit-animation-name:ivuSlideDownIn;animation-name:ivuSlideDownIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-down-leave-active{-webkit-animation-name:ivuSlideDownOut;animation-name:ivuSlideDownOut;-webkit-animation-play-state:running;animation-play-state:running}.slide-down-appear,.slide-down-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-down-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-left-appear,.slide-left-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-left-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-left-appear,.slide-left-enter-active{-webkit-animation-name:ivuSlideLeftIn;animation-name:ivuSlideLeftIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-left-leave-active{-webkit-animation-name:ivuSlideLeftOut;animation-name:ivuSlideLeftOut;-webkit-animation-play-state:running;animation-play-state:running}.slide-left-appear,.slide-left-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-left-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-right-appear,.slide-right-enter-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-right-leave-active{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-right-appear,.slide-right-enter-active{-webkit-animation-name:ivuSlideRightIn;animation-name:ivuSlideRightIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-right-leave-active{-webkit-animation-name:ivuSlideRightOut;animation-name:ivuSlideRightOut;-webkit-animation-play-state:running;animation-play-state:running}.slide-right-appear,.slide-right-enter-active{opacity:0;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}.slide-right-leave-active{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes ivuTransitionDropIn{0%{opacity:0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}100%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}}@keyframes ivuTransitionDropIn{0%{opacity:0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}100%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}}@-webkit-keyframes ivuTransitionDropOut{0%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}}@keyframes ivuTransitionDropOut{0%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}}@-webkit-keyframes ivuSlideUpIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(1);transform:scaleY(1)}}@keyframes ivuSlideUpIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(1);transform:scaleY(1)}}@-webkit-keyframes ivuSlideUpOut{0%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}}@keyframes ivuSlideUpOut{0%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleY(.8);transform:scaleY(.8)}}@-webkit-keyframes ivuSlideDownIn{0%{opacity:0;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(.8);transform:scaleY(.8)}100%{opacity:1;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(1);transform:scaleY(1)}}@keyframes ivuSlideDownIn{0%{opacity:0;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(.8);transform:scaleY(.8)}100%{opacity:1;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(1);transform:scaleY(1)}}@-webkit-keyframes ivuSlideDownOut{0%{opacity:1;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(.8);transform:scaleY(.8)}}@keyframes ivuSlideDownOut{0%{opacity:1;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform-origin:100% 100%;transform-origin:100% 100%;-webkit-transform:scaleY(.8);transform:scaleY(.8)}}@-webkit-keyframes ivuSlideLeftIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes ivuSlideLeftIn{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}100%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes ivuSlideLeftOut{0%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(1);transform:scaleX(1)}100%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}}@keyframes ivuSlideLeftOut{0%{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(1);transform:scaleX(1)}100%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}}@-webkit-keyframes ivuSlideRightIn{0%{opacity:0;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}100%{opacity:1;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes ivuSlideRightIn{0%{opacity:0;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}100%{opacity:1;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes ivuSlideRightOut{0%{opacity:1;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(1);transform:scaleX(1)}100%{opacity:0;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}}@keyframes ivuSlideRightOut{0%{opacity:1;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(1);transform:scaleX(1)}100%{opacity:0;-webkit-transform-origin:100% 0;transform-origin:100% 0;-webkit-transform:scaleX(.8);transform:scaleX(.8)}}.collapse-transition{-webkit-transition:.2s height ease-in-out,.2s padding-top ease-in-out,.2s padding-bottom ease-in-out;transition:.2s height ease-in-out,.2s padding-top ease-in-out,.2s padding-bottom ease-in-out}.ivu-btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;line-height:1.5;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;padding:5px 15px 6px;font-size:12px;border-radius:4px;-webkit-transition:color .2s linear,background-color .2s linear,border .2s linear,-webkit-box-shadow .2s linear;transition:color .2s linear,background-color .2s linear,border .2s linear,-webkit-box-shadow .2s linear;transition:color .2s linear,background-color .2s linear,border .2s linear,box-shadow .2s linear;transition:color .2s linear,background-color .2s linear,border .2s linear,box-shadow .2s linear,-webkit-box-shadow .2s linear;color:#515a6e;background-color:#fff;border-color:#dcdee2}.ivu-btn>.ivu-icon{line-height:1.5;vertical-align:middle}.ivu-btn-icon-only.ivu-btn-circle>.ivu-icon{vertical-align:baseline}.ivu-btn>span{vertical-align:middle}.ivu-btn,.ivu-btn:active,.ivu-btn:focus{outline:0}.ivu-btn:not([disabled]):hover{text-decoration:none}.ivu-btn:not([disabled]):active{outline:0}.ivu-btn.disabled,.ivu-btn[disabled]{cursor:not-allowed}.ivu-btn.disabled>*,.ivu-btn[disabled]>*{pointer-events:none}.ivu-btn-large{padding:6px 15px 6px 15px;font-size:14px;border-radius:4px}.ivu-btn-small{padding:1px 7px 2px;font-size:12px;border-radius:3px}.ivu-btn-icon-only{padding:5px 15px 6px;font-size:12px;border-radius:4px}.ivu-btn-icon-only.ivu-btn-small{padding:1px 7px 2px;font-size:12px;border-radius:3px}.ivu-btn-icon-only.ivu-btn-large{padding:6px 15px 6px 15px;font-size:14px;border-radius:4px}.ivu-btn>a:only-child{color:currentColor}.ivu-btn>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn:hover{color:#747b8b;background-color:#fff;border-color:#e3e5e8}.ivu-btn:hover>a:only-child{color:currentColor}.ivu-btn:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn.active,.ivu-btn:active{color:#4d5669;background-color:#f2f2f2;border-color:#f2f2f2}.ivu-btn.active>a:only-child,.ivu-btn:active>a:only-child{color:currentColor}.ivu-btn.active>a:only-child:after,.ivu-btn:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn.disabled,.ivu-btn.disabled.active,.ivu-btn.disabled:active,.ivu-btn.disabled:focus,.ivu-btn.disabled:hover,.ivu-btn[disabled],.ivu-btn[disabled].active,.ivu-btn[disabled]:active,.ivu-btn[disabled]:focus,.ivu-btn[disabled]:hover,fieldset[disabled] .ivu-btn,fieldset[disabled] .ivu-btn.active,fieldset[disabled] .ivu-btn:active,fieldset[disabled] .ivu-btn:focus,fieldset[disabled] .ivu-btn:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn.disabled.active>a:only-child,.ivu-btn.disabled:active>a:only-child,.ivu-btn.disabled:focus>a:only-child,.ivu-btn.disabled:hover>a:only-child,.ivu-btn.disabled>a:only-child,.ivu-btn[disabled].active>a:only-child,.ivu-btn[disabled]:active>a:only-child,.ivu-btn[disabled]:focus>a:only-child,.ivu-btn[disabled]:hover>a:only-child,.ivu-btn[disabled]>a:only-child,fieldset[disabled] .ivu-btn.active>a:only-child,fieldset[disabled] .ivu-btn:active>a:only-child,fieldset[disabled] .ivu-btn:focus>a:only-child,fieldset[disabled] .ivu-btn:hover>a:only-child,fieldset[disabled] .ivu-btn>a:only-child{color:currentColor}.ivu-btn.disabled.active>a:only-child:after,.ivu-btn.disabled:active>a:only-child:after,.ivu-btn.disabled:focus>a:only-child:after,.ivu-btn.disabled:hover>a:only-child:after,.ivu-btn.disabled>a:only-child:after,.ivu-btn[disabled].active>a:only-child:after,.ivu-btn[disabled]:active>a:only-child:after,.ivu-btn[disabled]:focus>a:only-child:after,.ivu-btn[disabled]:hover>a:only-child:after,.ivu-btn[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn.active>a:only-child:after,fieldset[disabled] .ivu-btn:active>a:only-child:after,fieldset[disabled] .ivu-btn:focus>a:only-child:after,fieldset[disabled] .ivu-btn:hover>a:only-child:after,fieldset[disabled] .ivu-btn>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn:hover{color:#57a3f3;background-color:#fff;border-color:#57a3f3}.ivu-btn:hover>a:only-child{color:currentColor}.ivu-btn:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn.active,.ivu-btn:active{color:#2b85e4;background-color:#fff;border-color:#2b85e4}.ivu-btn.active>a:only-child,.ivu-btn:active>a:only-child{color:currentColor}.ivu-btn.active>a:only-child:after,.ivu-btn:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn:focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-btn-long{width:100%}.ivu-btn>.ivu-icon+span,.ivu-btn>span+.ivu-icon{margin-left:4px}.ivu-btn-primary{color:#fff;background-color:#2d8cf0;border-color:#2d8cf0}.ivu-btn-primary>a:only-child{color:currentColor}.ivu-btn-primary>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-primary:hover{color:#fff;background-color:#57a3f3;border-color:#57a3f3}.ivu-btn-primary:hover>a:only-child{color:currentColor}.ivu-btn-primary:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-primary.active,.ivu-btn-primary:active{color:#f2f2f2;background-color:#2b85e4;border-color:#2b85e4}.ivu-btn-primary.active>a:only-child,.ivu-btn-primary:active>a:only-child{color:currentColor}.ivu-btn-primary.active>a:only-child:after,.ivu-btn-primary:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-primary.disabled,.ivu-btn-primary.disabled.active,.ivu-btn-primary.disabled:active,.ivu-btn-primary.disabled:focus,.ivu-btn-primary.disabled:hover,.ivu-btn-primary[disabled],.ivu-btn-primary[disabled].active,.ivu-btn-primary[disabled]:active,.ivu-btn-primary[disabled]:focus,.ivu-btn-primary[disabled]:hover,fieldset[disabled] .ivu-btn-primary,fieldset[disabled] .ivu-btn-primary.active,fieldset[disabled] .ivu-btn-primary:active,fieldset[disabled] .ivu-btn-primary:focus,fieldset[disabled] .ivu-btn-primary:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-primary.disabled.active>a:only-child,.ivu-btn-primary.disabled:active>a:only-child,.ivu-btn-primary.disabled:focus>a:only-child,.ivu-btn-primary.disabled:hover>a:only-child,.ivu-btn-primary.disabled>a:only-child,.ivu-btn-primary[disabled].active>a:only-child,.ivu-btn-primary[disabled]:active>a:only-child,.ivu-btn-primary[disabled]:focus>a:only-child,.ivu-btn-primary[disabled]:hover>a:only-child,.ivu-btn-primary[disabled]>a:only-child,fieldset[disabled] .ivu-btn-primary.active>a:only-child,fieldset[disabled] .ivu-btn-primary:active>a:only-child,fieldset[disabled] .ivu-btn-primary:focus>a:only-child,fieldset[disabled] .ivu-btn-primary:hover>a:only-child,fieldset[disabled] .ivu-btn-primary>a:only-child{color:currentColor}.ivu-btn-primary.disabled.active>a:only-child:after,.ivu-btn-primary.disabled:active>a:only-child:after,.ivu-btn-primary.disabled:focus>a:only-child:after,.ivu-btn-primary.disabled:hover>a:only-child:after,.ivu-btn-primary.disabled>a:only-child:after,.ivu-btn-primary[disabled].active>a:only-child:after,.ivu-btn-primary[disabled]:active>a:only-child:after,.ivu-btn-primary[disabled]:focus>a:only-child:after,.ivu-btn-primary[disabled]:hover>a:only-child:after,.ivu-btn-primary[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-primary.active>a:only-child:after,fieldset[disabled] .ivu-btn-primary:active>a:only-child:after,fieldset[disabled] .ivu-btn-primary:focus>a:only-child:after,fieldset[disabled] .ivu-btn-primary:hover>a:only-child:after,fieldset[disabled] .ivu-btn-primary>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-primary.active,.ivu-btn-primary:active,.ivu-btn-primary:hover{color:#fff}.ivu-btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary:not(:first-child):not(:last-child){border-right-color:#2b85e4;border-left-color:#2b85e4}.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary:first-child:not(:last-child){border-right-color:#2b85e4}.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary:first-child:not(:last-child)[disabled]{border-right-color:#dcdee2}.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary+.ivu-btn,.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary:last-child:not(:first-child){border-left-color:#2b85e4}.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary+.ivu-btn[disabled],.ivu-btn-group:not(.ivu-btn-group-vertical) .ivu-btn-primary:last-child:not(:first-child)[disabled]{border-left-color:#dcdee2}.ivu-btn-group-vertical .ivu-btn-primary:not(:first-child):not(:last-child){border-top-color:#2b85e4;border-bottom-color:#2b85e4}.ivu-btn-group-vertical .ivu-btn-primary:first-child:not(:last-child){border-bottom-color:#2b85e4}.ivu-btn-group-vertical .ivu-btn-primary:first-child:not(:last-child)[disabled]{border-top-color:#dcdee2}.ivu-btn-group-vertical .ivu-btn-primary+.ivu-btn,.ivu-btn-group-vertical .ivu-btn-primary:last-child:not(:first-child){border-top-color:#2b85e4}.ivu-btn-group-vertical .ivu-btn-primary+.ivu-btn[disabled],.ivu-btn-group-vertical .ivu-btn-primary:last-child:not(:first-child)[disabled]{border-bottom-color:#dcdee2}.ivu-btn-dashed{color:#515a6e;background-color:#fff;border-color:#dcdee2;border-style:dashed}.ivu-btn-dashed>a:only-child{color:currentColor}.ivu-btn-dashed>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-dashed:hover{color:#747b8b;background-color:#fff;border-color:#e3e5e8}.ivu-btn-dashed:hover>a:only-child{color:currentColor}.ivu-btn-dashed:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-dashed.active,.ivu-btn-dashed:active{color:#4d5669;background-color:#f2f2f2;border-color:#f2f2f2}.ivu-btn-dashed.active>a:only-child,.ivu-btn-dashed:active>a:only-child{color:currentColor}.ivu-btn-dashed.active>a:only-child:after,.ivu-btn-dashed:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-dashed.disabled,.ivu-btn-dashed.disabled.active,.ivu-btn-dashed.disabled:active,.ivu-btn-dashed.disabled:focus,.ivu-btn-dashed.disabled:hover,.ivu-btn-dashed[disabled],.ivu-btn-dashed[disabled].active,.ivu-btn-dashed[disabled]:active,.ivu-btn-dashed[disabled]:focus,.ivu-btn-dashed[disabled]:hover,fieldset[disabled] .ivu-btn-dashed,fieldset[disabled] .ivu-btn-dashed.active,fieldset[disabled] .ivu-btn-dashed:active,fieldset[disabled] .ivu-btn-dashed:focus,fieldset[disabled] .ivu-btn-dashed:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-dashed.disabled.active>a:only-child,.ivu-btn-dashed.disabled:active>a:only-child,.ivu-btn-dashed.disabled:focus>a:only-child,.ivu-btn-dashed.disabled:hover>a:only-child,.ivu-btn-dashed.disabled>a:only-child,.ivu-btn-dashed[disabled].active>a:only-child,.ivu-btn-dashed[disabled]:active>a:only-child,.ivu-btn-dashed[disabled]:focus>a:only-child,.ivu-btn-dashed[disabled]:hover>a:only-child,.ivu-btn-dashed[disabled]>a:only-child,fieldset[disabled] .ivu-btn-dashed.active>a:only-child,fieldset[disabled] .ivu-btn-dashed:active>a:only-child,fieldset[disabled] .ivu-btn-dashed:focus>a:only-child,fieldset[disabled] .ivu-btn-dashed:hover>a:only-child,fieldset[disabled] .ivu-btn-dashed>a:only-child{color:currentColor}.ivu-btn-dashed.disabled.active>a:only-child:after,.ivu-btn-dashed.disabled:active>a:only-child:after,.ivu-btn-dashed.disabled:focus>a:only-child:after,.ivu-btn-dashed.disabled:hover>a:only-child:after,.ivu-btn-dashed.disabled>a:only-child:after,.ivu-btn-dashed[disabled].active>a:only-child:after,.ivu-btn-dashed[disabled]:active>a:only-child:after,.ivu-btn-dashed[disabled]:focus>a:only-child:after,.ivu-btn-dashed[disabled]:hover>a:only-child:after,.ivu-btn-dashed[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-dashed.active>a:only-child:after,fieldset[disabled] .ivu-btn-dashed:active>a:only-child:after,fieldset[disabled] .ivu-btn-dashed:focus>a:only-child:after,fieldset[disabled] .ivu-btn-dashed:hover>a:only-child:after,fieldset[disabled] .ivu-btn-dashed>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-dashed:hover{color:#57a3f3;background-color:#fff;border-color:#57a3f3}.ivu-btn-dashed:hover>a:only-child{color:currentColor}.ivu-btn-dashed:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-dashed.active,.ivu-btn-dashed:active{color:#2b85e4;background-color:#fff;border-color:#2b85e4}.ivu-btn-dashed.active>a:only-child,.ivu-btn-dashed:active>a:only-child{color:currentColor}.ivu-btn-dashed.active>a:only-child:after,.ivu-btn-dashed:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-dashed:focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-btn-text{color:#515a6e;background-color:transparent;border-color:transparent}.ivu-btn-text>a:only-child{color:currentColor}.ivu-btn-text>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text:hover{color:#747b8b;background-color:rgba(255,255,255,.2);border-color:rgba(255,255,255,.2)}.ivu-btn-text:hover>a:only-child{color:currentColor}.ivu-btn-text:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text.active,.ivu-btn-text:active{color:#4d5669;background-color:rgba(0,0,0,.05);border-color:rgba(0,0,0,.05)}.ivu-btn-text.active>a:only-child,.ivu-btn-text:active>a:only-child{color:currentColor}.ivu-btn-text.active>a:only-child:after,.ivu-btn-text:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text.disabled,.ivu-btn-text.disabled.active,.ivu-btn-text.disabled:active,.ivu-btn-text.disabled:focus,.ivu-btn-text.disabled:hover,.ivu-btn-text[disabled],.ivu-btn-text[disabled].active,.ivu-btn-text[disabled]:active,.ivu-btn-text[disabled]:focus,.ivu-btn-text[disabled]:hover,fieldset[disabled] .ivu-btn-text,fieldset[disabled] .ivu-btn-text.active,fieldset[disabled] .ivu-btn-text:active,fieldset[disabled] .ivu-btn-text:focus,fieldset[disabled] .ivu-btn-text:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-text.disabled.active>a:only-child,.ivu-btn-text.disabled:active>a:only-child,.ivu-btn-text.disabled:focus>a:only-child,.ivu-btn-text.disabled:hover>a:only-child,.ivu-btn-text.disabled>a:only-child,.ivu-btn-text[disabled].active>a:only-child,.ivu-btn-text[disabled]:active>a:only-child,.ivu-btn-text[disabled]:focus>a:only-child,.ivu-btn-text[disabled]:hover>a:only-child,.ivu-btn-text[disabled]>a:only-child,fieldset[disabled] .ivu-btn-text.active>a:only-child,fieldset[disabled] .ivu-btn-text:active>a:only-child,fieldset[disabled] .ivu-btn-text:focus>a:only-child,fieldset[disabled] .ivu-btn-text:hover>a:only-child,fieldset[disabled] .ivu-btn-text>a:only-child{color:currentColor}.ivu-btn-text.disabled.active>a:only-child:after,.ivu-btn-text.disabled:active>a:only-child:after,.ivu-btn-text.disabled:focus>a:only-child:after,.ivu-btn-text.disabled:hover>a:only-child:after,.ivu-btn-text.disabled>a:only-child:after,.ivu-btn-text[disabled].active>a:only-child:after,.ivu-btn-text[disabled]:active>a:only-child:after,.ivu-btn-text[disabled]:focus>a:only-child:after,.ivu-btn-text[disabled]:hover>a:only-child:after,.ivu-btn-text[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-text.active>a:only-child:after,fieldset[disabled] .ivu-btn-text:active>a:only-child:after,fieldset[disabled] .ivu-btn-text:focus>a:only-child:after,fieldset[disabled] .ivu-btn-text:hover>a:only-child:after,fieldset[disabled] .ivu-btn-text>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text.disabled,.ivu-btn-text.disabled.active,.ivu-btn-text.disabled:active,.ivu-btn-text.disabled:focus,.ivu-btn-text.disabled:hover,.ivu-btn-text[disabled],.ivu-btn-text[disabled].active,.ivu-btn-text[disabled]:active,.ivu-btn-text[disabled]:focus,.ivu-btn-text[disabled]:hover,fieldset[disabled] .ivu-btn-text,fieldset[disabled] .ivu-btn-text.active,fieldset[disabled] .ivu-btn-text:active,fieldset[disabled] .ivu-btn-text:focus,fieldset[disabled] .ivu-btn-text:hover{color:#c5c8ce;background-color:#fff;border-color:transparent}.ivu-btn-text.disabled.active>a:only-child,.ivu-btn-text.disabled:active>a:only-child,.ivu-btn-text.disabled:focus>a:only-child,.ivu-btn-text.disabled:hover>a:only-child,.ivu-btn-text.disabled>a:only-child,.ivu-btn-text[disabled].active>a:only-child,.ivu-btn-text[disabled]:active>a:only-child,.ivu-btn-text[disabled]:focus>a:only-child,.ivu-btn-text[disabled]:hover>a:only-child,.ivu-btn-text[disabled]>a:only-child,fieldset[disabled] .ivu-btn-text.active>a:only-child,fieldset[disabled] .ivu-btn-text:active>a:only-child,fieldset[disabled] .ivu-btn-text:focus>a:only-child,fieldset[disabled] .ivu-btn-text:hover>a:only-child,fieldset[disabled] .ivu-btn-text>a:only-child{color:currentColor}.ivu-btn-text.disabled.active>a:only-child:after,.ivu-btn-text.disabled:active>a:only-child:after,.ivu-btn-text.disabled:focus>a:only-child:after,.ivu-btn-text.disabled:hover>a:only-child:after,.ivu-btn-text.disabled>a:only-child:after,.ivu-btn-text[disabled].active>a:only-child:after,.ivu-btn-text[disabled]:active>a:only-child:after,.ivu-btn-text[disabled]:focus>a:only-child:after,.ivu-btn-text[disabled]:hover>a:only-child:after,.ivu-btn-text[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-text.active>a:only-child:after,fieldset[disabled] .ivu-btn-text:active>a:only-child:after,fieldset[disabled] .ivu-btn-text:focus>a:only-child:after,fieldset[disabled] .ivu-btn-text:hover>a:only-child:after,fieldset[disabled] .ivu-btn-text>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text:hover{color:#57a3f3;background-color:#fff;border-color:transparent}.ivu-btn-text:hover>a:only-child{color:currentColor}.ivu-btn-text:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text.active,.ivu-btn-text:active{color:#2b85e4;background-color:#fff;border-color:transparent}.ivu-btn-text.active>a:only-child,.ivu-btn-text:active>a:only-child{color:currentColor}.ivu-btn-text.active>a:only-child:after,.ivu-btn-text:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-text:focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-btn-success{color:#fff;background-color:#19be6b;border-color:#19be6b}.ivu-btn-success>a:only-child{color:currentColor}.ivu-btn-success>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-success:hover{color:#fff;background-color:#47cb89;border-color:#47cb89}.ivu-btn-success:hover>a:only-child{color:currentColor}.ivu-btn-success:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-success.active,.ivu-btn-success:active{color:#f2f2f2;background-color:#18b566;border-color:#18b566}.ivu-btn-success.active>a:only-child,.ivu-btn-success:active>a:only-child{color:currentColor}.ivu-btn-success.active>a:only-child:after,.ivu-btn-success:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-success.disabled,.ivu-btn-success.disabled.active,.ivu-btn-success.disabled:active,.ivu-btn-success.disabled:focus,.ivu-btn-success.disabled:hover,.ivu-btn-success[disabled],.ivu-btn-success[disabled].active,.ivu-btn-success[disabled]:active,.ivu-btn-success[disabled]:focus,.ivu-btn-success[disabled]:hover,fieldset[disabled] .ivu-btn-success,fieldset[disabled] .ivu-btn-success.active,fieldset[disabled] .ivu-btn-success:active,fieldset[disabled] .ivu-btn-success:focus,fieldset[disabled] .ivu-btn-success:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-success.disabled.active>a:only-child,.ivu-btn-success.disabled:active>a:only-child,.ivu-btn-success.disabled:focus>a:only-child,.ivu-btn-success.disabled:hover>a:only-child,.ivu-btn-success.disabled>a:only-child,.ivu-btn-success[disabled].active>a:only-child,.ivu-btn-success[disabled]:active>a:only-child,.ivu-btn-success[disabled]:focus>a:only-child,.ivu-btn-success[disabled]:hover>a:only-child,.ivu-btn-success[disabled]>a:only-child,fieldset[disabled] .ivu-btn-success.active>a:only-child,fieldset[disabled] .ivu-btn-success:active>a:only-child,fieldset[disabled] .ivu-btn-success:focus>a:only-child,fieldset[disabled] .ivu-btn-success:hover>a:only-child,fieldset[disabled] .ivu-btn-success>a:only-child{color:currentColor}.ivu-btn-success.disabled.active>a:only-child:after,.ivu-btn-success.disabled:active>a:only-child:after,.ivu-btn-success.disabled:focus>a:only-child:after,.ivu-btn-success.disabled:hover>a:only-child:after,.ivu-btn-success.disabled>a:only-child:after,.ivu-btn-success[disabled].active>a:only-child:after,.ivu-btn-success[disabled]:active>a:only-child:after,.ivu-btn-success[disabled]:focus>a:only-child:after,.ivu-btn-success[disabled]:hover>a:only-child:after,.ivu-btn-success[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-success.active>a:only-child:after,fieldset[disabled] .ivu-btn-success:active>a:only-child:after,fieldset[disabled] .ivu-btn-success:focus>a:only-child:after,fieldset[disabled] .ivu-btn-success:hover>a:only-child:after,fieldset[disabled] .ivu-btn-success>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-success.active,.ivu-btn-success:active,.ivu-btn-success:hover{color:#fff}.ivu-btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(25,190,107,.2);box-shadow:0 0 0 2px rgba(25,190,107,.2)}.ivu-btn-warning{color:#fff;background-color:#f90;border-color:#f90}.ivu-btn-warning>a:only-child{color:currentColor}.ivu-btn-warning>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-warning:hover{color:#fff;background-color:#ffad33;border-color:#ffad33}.ivu-btn-warning:hover>a:only-child{color:currentColor}.ivu-btn-warning:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-warning.active,.ivu-btn-warning:active{color:#f2f2f2;background-color:#f29100;border-color:#f29100}.ivu-btn-warning.active>a:only-child,.ivu-btn-warning:active>a:only-child{color:currentColor}.ivu-btn-warning.active>a:only-child:after,.ivu-btn-warning:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-warning.disabled,.ivu-btn-warning.disabled.active,.ivu-btn-warning.disabled:active,.ivu-btn-warning.disabled:focus,.ivu-btn-warning.disabled:hover,.ivu-btn-warning[disabled],.ivu-btn-warning[disabled].active,.ivu-btn-warning[disabled]:active,.ivu-btn-warning[disabled]:focus,.ivu-btn-warning[disabled]:hover,fieldset[disabled] .ivu-btn-warning,fieldset[disabled] .ivu-btn-warning.active,fieldset[disabled] .ivu-btn-warning:active,fieldset[disabled] .ivu-btn-warning:focus,fieldset[disabled] .ivu-btn-warning:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-warning.disabled.active>a:only-child,.ivu-btn-warning.disabled:active>a:only-child,.ivu-btn-warning.disabled:focus>a:only-child,.ivu-btn-warning.disabled:hover>a:only-child,.ivu-btn-warning.disabled>a:only-child,.ivu-btn-warning[disabled].active>a:only-child,.ivu-btn-warning[disabled]:active>a:only-child,.ivu-btn-warning[disabled]:focus>a:only-child,.ivu-btn-warning[disabled]:hover>a:only-child,.ivu-btn-warning[disabled]>a:only-child,fieldset[disabled] .ivu-btn-warning.active>a:only-child,fieldset[disabled] .ivu-btn-warning:active>a:only-child,fieldset[disabled] .ivu-btn-warning:focus>a:only-child,fieldset[disabled] .ivu-btn-warning:hover>a:only-child,fieldset[disabled] .ivu-btn-warning>a:only-child{color:currentColor}.ivu-btn-warning.disabled.active>a:only-child:after,.ivu-btn-warning.disabled:active>a:only-child:after,.ivu-btn-warning.disabled:focus>a:only-child:after,.ivu-btn-warning.disabled:hover>a:only-child:after,.ivu-btn-warning.disabled>a:only-child:after,.ivu-btn-warning[disabled].active>a:only-child:after,.ivu-btn-warning[disabled]:active>a:only-child:after,.ivu-btn-warning[disabled]:focus>a:only-child:after,.ivu-btn-warning[disabled]:hover>a:only-child:after,.ivu-btn-warning[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-warning.active>a:only-child:after,fieldset[disabled] .ivu-btn-warning:active>a:only-child:after,fieldset[disabled] .ivu-btn-warning:focus>a:only-child:after,fieldset[disabled] .ivu-btn-warning:hover>a:only-child:after,fieldset[disabled] .ivu-btn-warning>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-warning.active,.ivu-btn-warning:active,.ivu-btn-warning:hover{color:#fff}.ivu-btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(255,153,0,.2);box-shadow:0 0 0 2px rgba(255,153,0,.2)}.ivu-btn-error{color:#fff;background-color:#ed4014;border-color:#ed4014}.ivu-btn-error>a:only-child{color:currentColor}.ivu-btn-error>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-error:hover{color:#fff;background-color:#f16643;border-color:#f16643}.ivu-btn-error:hover>a:only-child{color:currentColor}.ivu-btn-error:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-error.active,.ivu-btn-error:active{color:#f2f2f2;background-color:#e13d13;border-color:#e13d13}.ivu-btn-error.active>a:only-child,.ivu-btn-error:active>a:only-child{color:currentColor}.ivu-btn-error.active>a:only-child:after,.ivu-btn-error:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-error.disabled,.ivu-btn-error.disabled.active,.ivu-btn-error.disabled:active,.ivu-btn-error.disabled:focus,.ivu-btn-error.disabled:hover,.ivu-btn-error[disabled],.ivu-btn-error[disabled].active,.ivu-btn-error[disabled]:active,.ivu-btn-error[disabled]:focus,.ivu-btn-error[disabled]:hover,fieldset[disabled] .ivu-btn-error,fieldset[disabled] .ivu-btn-error.active,fieldset[disabled] .ivu-btn-error:active,fieldset[disabled] .ivu-btn-error:focus,fieldset[disabled] .ivu-btn-error:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-error.disabled.active>a:only-child,.ivu-btn-error.disabled:active>a:only-child,.ivu-btn-error.disabled:focus>a:only-child,.ivu-btn-error.disabled:hover>a:only-child,.ivu-btn-error.disabled>a:only-child,.ivu-btn-error[disabled].active>a:only-child,.ivu-btn-error[disabled]:active>a:only-child,.ivu-btn-error[disabled]:focus>a:only-child,.ivu-btn-error[disabled]:hover>a:only-child,.ivu-btn-error[disabled]>a:only-child,fieldset[disabled] .ivu-btn-error.active>a:only-child,fieldset[disabled] .ivu-btn-error:active>a:only-child,fieldset[disabled] .ivu-btn-error:focus>a:only-child,fieldset[disabled] .ivu-btn-error:hover>a:only-child,fieldset[disabled] .ivu-btn-error>a:only-child{color:currentColor}.ivu-btn-error.disabled.active>a:only-child:after,.ivu-btn-error.disabled:active>a:only-child:after,.ivu-btn-error.disabled:focus>a:only-child:after,.ivu-btn-error.disabled:hover>a:only-child:after,.ivu-btn-error.disabled>a:only-child:after,.ivu-btn-error[disabled].active>a:only-child:after,.ivu-btn-error[disabled]:active>a:only-child:after,.ivu-btn-error[disabled]:focus>a:only-child:after,.ivu-btn-error[disabled]:hover>a:only-child:after,.ivu-btn-error[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-error.active>a:only-child:after,fieldset[disabled] .ivu-btn-error:active>a:only-child:after,fieldset[disabled] .ivu-btn-error:focus>a:only-child:after,fieldset[disabled] .ivu-btn-error:hover>a:only-child:after,fieldset[disabled] .ivu-btn-error>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-error.active,.ivu-btn-error:active,.ivu-btn-error:hover{color:#fff}.ivu-btn-error:focus{-webkit-box-shadow:0 0 0 2px rgba(237,64,20,.2);box-shadow:0 0 0 2px rgba(237,64,20,.2)}.ivu-btn-info{color:#fff;background-color:#2db7f5;border-color:#2db7f5}.ivu-btn-info>a:only-child{color:currentColor}.ivu-btn-info>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-info:hover{color:#fff;background-color:#57c5f7;border-color:#57c5f7}.ivu-btn-info:hover>a:only-child{color:currentColor}.ivu-btn-info:hover>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-info.active,.ivu-btn-info:active{color:#f2f2f2;background-color:#2baee9;border-color:#2baee9}.ivu-btn-info.active>a:only-child,.ivu-btn-info:active>a:only-child{color:currentColor}.ivu-btn-info.active>a:only-child:after,.ivu-btn-info:active>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-info.disabled,.ivu-btn-info.disabled.active,.ivu-btn-info.disabled:active,.ivu-btn-info.disabled:focus,.ivu-btn-info.disabled:hover,.ivu-btn-info[disabled],.ivu-btn-info[disabled].active,.ivu-btn-info[disabled]:active,.ivu-btn-info[disabled]:focus,.ivu-btn-info[disabled]:hover,fieldset[disabled] .ivu-btn-info,fieldset[disabled] .ivu-btn-info.active,fieldset[disabled] .ivu-btn-info:active,fieldset[disabled] .ivu-btn-info:focus,fieldset[disabled] .ivu-btn-info:hover{color:#c5c8ce;background-color:#f7f7f7;border-color:#dcdee2}.ivu-btn-info.disabled.active>a:only-child,.ivu-btn-info.disabled:active>a:only-child,.ivu-btn-info.disabled:focus>a:only-child,.ivu-btn-info.disabled:hover>a:only-child,.ivu-btn-info.disabled>a:only-child,.ivu-btn-info[disabled].active>a:only-child,.ivu-btn-info[disabled]:active>a:only-child,.ivu-btn-info[disabled]:focus>a:only-child,.ivu-btn-info[disabled]:hover>a:only-child,.ivu-btn-info[disabled]>a:only-child,fieldset[disabled] .ivu-btn-info.active>a:only-child,fieldset[disabled] .ivu-btn-info:active>a:only-child,fieldset[disabled] .ivu-btn-info:focus>a:only-child,fieldset[disabled] .ivu-btn-info:hover>a:only-child,fieldset[disabled] .ivu-btn-info>a:only-child{color:currentColor}.ivu-btn-info.disabled.active>a:only-child:after,.ivu-btn-info.disabled:active>a:only-child:after,.ivu-btn-info.disabled:focus>a:only-child:after,.ivu-btn-info.disabled:hover>a:only-child:after,.ivu-btn-info.disabled>a:only-child:after,.ivu-btn-info[disabled].active>a:only-child:after,.ivu-btn-info[disabled]:active>a:only-child:after,.ivu-btn-info[disabled]:focus>a:only-child:after,.ivu-btn-info[disabled]:hover>a:only-child:after,.ivu-btn-info[disabled]>a:only-child:after,fieldset[disabled] .ivu-btn-info.active>a:only-child:after,fieldset[disabled] .ivu-btn-info:active>a:only-child:after,fieldset[disabled] .ivu-btn-info:focus>a:only-child:after,fieldset[disabled] .ivu-btn-info:hover>a:only-child:after,fieldset[disabled] .ivu-btn-info>a:only-child:after{content:'';position:absolute;top:0;left:0;bottom:0;right:0;background:0 0}.ivu-btn-info.active,.ivu-btn-info:active,.ivu-btn-info:hover{color:#fff}.ivu-btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(45,183,245,.2);box-shadow:0 0 0 2px rgba(45,183,245,.2)}.ivu-btn-circle,.ivu-btn-circle-outline{border-radius:32px}.ivu-btn-circle-outline.ivu-btn-large,.ivu-btn-circle.ivu-btn-large{border-radius:36px}.ivu-btn-circle-outline.ivu-btn-size,.ivu-btn-circle.ivu-btn-size{border-radius:24px}.ivu-btn-circle-outline.ivu-btn-icon-only,.ivu-btn-circle.ivu-btn-icon-only{width:32px;height:32px;padding:0;font-size:16px;border-radius:50%}.ivu-btn-circle-outline.ivu-btn-icon-only.ivu-btn-large,.ivu-btn-circle.ivu-btn-icon-only.ivu-btn-large{width:36px;height:36px;padding:0;font-size:16px;border-radius:50%}.ivu-btn-circle-outline.ivu-btn-icon-only.ivu-btn-small,.ivu-btn-circle.ivu-btn-icon-only.ivu-btn-small{width:24px;height:24px;padding:0;font-size:14px;border-radius:50%}.ivu-btn:before{position:absolute;top:-1px;left:-1px;bottom:-1px;right:-1px;background:#fff;opacity:.35;content:'';border-radius:inherit;z-index:1;-webkit-transition:opacity .2s;transition:opacity .2s;pointer-events:none;display:none}.ivu-btn.ivu-btn-loading{pointer-events:none;position:relative}.ivu-btn.ivu-btn-loading:before{display:block}.ivu-btn-group{position:relative;display:inline-block;vertical-align:middle}.ivu-btn-group>.ivu-btn{position:relative;float:left}.ivu-btn-group>.ivu-btn.active,.ivu-btn-group>.ivu-btn:active,.ivu-btn-group>.ivu-btn:hover{z-index:2}.ivu-btn-group .ivu-btn-icon-only .ivu-icon{font-size:13px;position:relative}.ivu-btn-group-large .ivu-btn-icon-only .ivu-icon{font-size:15px}.ivu-btn-group-small .ivu-btn-icon-only .ivu-icon{font-size:12px}.ivu-btn-group-circle .ivu-btn{border-radius:32px}.ivu-btn-group-large.ivu-btn-group-circle .ivu-btn{border-radius:36px}.ivu-btn-group-large>.ivu-btn{padding:6px 15px 6px 15px;font-size:14px;border-radius:4px}.ivu-btn-group-small.ivu-btn-group-circle .ivu-btn{border-radius:24px}.ivu-btn-group-small>.ivu-btn{padding:1px 7px 2px;font-size:12px;border-radius:3px}.ivu-btn-group-small>.ivu-btn>.ivu-icon{font-size:12px}.ivu-btn+.ivu-btn-group,.ivu-btn-group .ivu-btn+.ivu-btn,.ivu-btn-group+.ivu-btn,.ivu-btn-group+.ivu-btn-group{margin-left:-1px}.ivu-btn-group .ivu-btn:not(:first-child):not(:last-child){border-radius:0}.ivu-btn-group:not(.ivu-btn-group-vertical)>.ivu-btn:first-child{margin-left:0}.ivu-btn-group:not(.ivu-btn-group-vertical)>.ivu-btn:first-child:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.ivu-btn-group:not(.ivu-btn-group-vertical)>.ivu-btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.ivu-btn-group>.ivu-btn-group{float:left}.ivu-btn-group>.ivu-btn-group:not(:first-child):not(:last-child)>.ivu-btn{border-radius:0}.ivu-btn-group:not(.ivu-btn-group-vertical)>.ivu-btn-group:first-child:not(:last-child)>.ivu-btn:last-child{border-bottom-right-radius:0;border-top-right-radius:0;padding-right:8px}.ivu-btn-group:not(.ivu-btn-group-vertical)>.ivu-btn-group:last-child:not(:first-child)>.ivu-btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0;padding-left:8px}.ivu-btn-group-vertical{display:inline-block;vertical-align:middle}.ivu-btn-group-vertical>.ivu-btn{display:block;width:100%;max-width:100%;float:none}.ivu-btn+.ivu-btn-group-vertical,.ivu-btn-group-vertical .ivu-btn+.ivu-btn,.ivu-btn-group-vertical+.ivu-btn,.ivu-btn-group-vertical+.ivu-btn-group-vertical{margin-top:-1px;margin-left:0}.ivu-btn-group-vertical>.ivu-btn:first-child{margin-top:0}.ivu-btn-group-vertical>.ivu-btn:first-child:not(:last-child){border-bottom-left-radius:0;border-bottom-right-radius:0}.ivu-btn-group-vertical>.ivu-btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.ivu-btn-group-vertical>.ivu-btn-group-vertical:first-child:not(:last-child)>.ivu-btn:last-child{border-bottom-left-radius:0;border-bottom-right-radius:0;padding-bottom:8px}.ivu-btn-group-vertical>.ivu-btn-group-vertical:last-child:not(:first-child)>.ivu-btn:first-child{border-bottom-right-radius:0;border-bottom-left-radius:0;padding-top:8px}.ivu-btn-ghost{color:#fff;background:0 0}.ivu-btn-ghost:hover{background:0 0}.ivu-btn-ghost.ivu-btn-dashed,.ivu-btn-ghost.ivu-btn-default{color:#fff;border-color:#fff}.ivu-btn-ghost.ivu-btn-dashed:hover,.ivu-btn-ghost.ivu-btn-default:hover{color:#57a3f3;border-color:#57a3f3}.ivu-btn-ghost.ivu-btn-primary{color:#2d8cf0}.ivu-btn-ghost.ivu-btn-primary:hover{color:#57a3f3;background:rgba(245,249,254,.5)}.ivu-btn-ghost.ivu-btn-info{color:#2db7f5}.ivu-btn-ghost.ivu-btn-info:hover{color:#57c5f7;background:rgba(245,251,254,.5)}.ivu-btn-ghost.ivu-btn-success{color:#19be6b}.ivu-btn-ghost.ivu-btn-success:hover{color:#47cb89;background:rgba(244,252,248,.5)}.ivu-btn-ghost.ivu-btn-warning{color:#f90}.ivu-btn-ghost.ivu-btn-warning:hover{color:#ffad33;background:rgba(255,250,242,.5)}.ivu-btn-ghost.ivu-btn-error{color:#ed4014}.ivu-btn-ghost.ivu-btn-error:hover{color:#f16643;background:rgba(254,245,243,.5)}.ivu-btn-ghost.ivu-btn-dashed[disabled],.ivu-btn-ghost.ivu-btn-default[disabled],.ivu-btn-ghost.ivu-btn-error[disabled],.ivu-btn-ghost.ivu-btn-info[disabled],.ivu-btn-ghost.ivu-btn-primary[disabled],.ivu-btn-ghost.ivu-btn-success[disabled],.ivu-btn-ghost.ivu-btn-warning[disabled]{background:0 0;color:rgba(0,0,0,.25);border-color:#dcdee2}.ivu-btn-ghost.ivu-btn-text[disabled]{background:0 0;color:rgba(0,0,0,.25)}.ivu-affix{position:fixed;z-index:10}.ivu-back-top{z-index:10;position:fixed;cursor:pointer;display:none}.ivu-back-top.ivu-back-top-show{display:block}.ivu-back-top-inner{background-color:rgba(0,0,0,.6);border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.2);box-shadow:0 1px 3px rgba(0,0,0,.2);-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-back-top-inner:hover{background-color:rgba(0,0,0,.7)}.ivu-back-top i{color:#fff;font-size:24px;padding:8px 12px}.ivu-badge{position:relative;display:inline-block}.ivu-badge-count{font-family:"Monospaced Number";line-height:1;vertical-align:middle;position:absolute;-webkit-transform:translateX(50%);-ms-transform:translateX(50%);transform:translateX(50%);top:-10px;right:0;height:20px;border-radius:10px;min-width:20px;background:#ed4014;border:1px solid transparent;color:#fff;line-height:18px;text-align:center;padding:0 6px;font-size:12px;white-space:nowrap;-webkit-transform-origin:-10% center;-ms-transform-origin:-10% center;transform-origin:-10% center;z-index:10;-webkit-box-shadow:0 0 0 1px #fff;box-shadow:0 0 0 1px #fff}.ivu-badge-count a,.ivu-badge-count a:hover{color:#fff}.ivu-badge-count-alone{top:auto;display:block;position:relative;-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}.ivu-badge-count-primary{background:#2d8cf0}.ivu-badge-count-success{background:#19be6b}.ivu-badge-count-error{background:#ed4014}.ivu-badge-count-warning{background:#f90}.ivu-badge-count-info{background:#2db7f5}.ivu-badge-count-normal{background:#e6ebf1;color:#808695}.ivu-badge-dot{position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);-webkit-transform-origin:0 center;-ms-transform-origin:0 center;transform-origin:0 center;top:-4px;right:-8px;height:8px;width:8px;border-radius:100%;background:#ed4014;z-index:10;-webkit-box-shadow:0 0 0 1px #fff;box-shadow:0 0 0 1px #fff}.ivu-badge-status{line-height:inherit;vertical-align:baseline}.ivu-badge-status-dot{width:6px;height:6px;display:inline-block;border-radius:50%;vertical-align:middle;position:relative;top:-1px}.ivu-badge-status-success{background-color:#19be6b}.ivu-badge-status-processing{background-color:#2d8cf0;position:relative}.ivu-badge-status-processing:after{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:50%;border:1px solid #2d8cf0;content:'';-webkit-animation:aniStatusProcessing 1.2s infinite ease-in-out;animation:aniStatusProcessing 1.2s infinite ease-in-out}.ivu-badge-status-default{background-color:#e6ebf1}.ivu-badge-status-error{background-color:#ed4014}.ivu-badge-status-warning{background-color:#f90}.ivu-badge-status-text{display:inline-block;color:#515a6e;font-size:12px;margin-left:6px}@-webkit-keyframes aniStatusProcessing{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:.5}100%{-webkit-transform:scale(2.4);transform:scale(2.4);opacity:0}}@keyframes aniStatusProcessing{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:.5}100%{-webkit-transform:scale(2.4);transform:scale(2.4);opacity:0}}.ivu-chart-circle{display:inline-block;position:relative}.ivu-chart-circle-inner{width:100%;text-align:center;position:absolute;left:0;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);line-height:1}.ivu-spin{color:#2d8cf0;vertical-align:middle;text-align:center}.ivu-spin-dot{position:relative;display:block;border-radius:50%;background-color:#2d8cf0;width:20px;height:20px;-webkit-animation:ani-spin-bounce 1s 0s ease-in-out infinite;animation:ani-spin-bounce 1s 0s ease-in-out infinite}.ivu-spin-large .ivu-spin-dot{width:32px;height:32px}.ivu-spin-small .ivu-spin-dot{width:12px;height:12px}.ivu-spin-fix{position:absolute;top:0;left:0;z-index:8;width:100%;height:100%;background-color:rgba(255,255,255,.9)}.ivu-spin-fullscreen{z-index:2010}.ivu-spin-fullscreen-wrapper{position:fixed;top:0;right:0;bottom:0;left:0}.ivu-spin-fix .ivu-spin-main{position:absolute;top:50%;left:50%;-ms-transform:translate(-50%,-50%);-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.ivu-spin-fix .ivu-spin-dot{display:inline-block}.ivu-spin-show-text .ivu-spin-dot,.ivu-spin-text{display:none}.ivu-spin-show-text .ivu-spin-text{display:block}.ivu-table-wrapper>.ivu-spin-fix{border:1px solid #dcdee2;border-top:0;border-left:0}@-webkit-keyframes ani-spin-bounce{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:0}}@keyframes ani-spin-bounce{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:0}}.ivu-alert{position:relative;padding:8px 48px 8px 16px;border-radius:4px;color:#515a6e;font-size:12px;line-height:16px;margin-bottom:10px}.ivu-alert.ivu-alert-with-icon{padding:8px 48px 8px 38px}.ivu-alert-icon{font-size:16px;top:6px;left:12px;position:absolute}.ivu-alert-desc{font-size:12px;color:#515a6e;line-height:21px;display:none;text-align:justify}.ivu-alert-success{border:1px solid #8ce6b0;background-color:#edfff3}.ivu-alert-success .ivu-alert-icon{color:#19be6b}.ivu-alert-info{border:1px solid #abdcff;background-color:#f0faff}.ivu-alert-info .ivu-alert-icon{color:#2d8cf0}.ivu-alert-warning{border:1px solid #ffd77a;background-color:#fff9e6}.ivu-alert-warning .ivu-alert-icon{color:#f90}.ivu-alert-error{border:1px solid #ffb08f;background-color:#ffefe6}.ivu-alert-error .ivu-alert-icon{color:#ed4014}.ivu-alert-close{font-size:12px;position:absolute;right:8px;top:8px;overflow:hidden;cursor:pointer}.ivu-alert-close .ivu-icon-ios-close{font-size:22px;color:#999;-webkit-transition:color .2s ease;transition:color .2s ease;position:relative;top:-3px}.ivu-alert-close .ivu-icon-ios-close:hover{color:#444}.ivu-alert-with-desc{padding:16px;position:relative;border-radius:4px;margin-bottom:10px;color:#515a6e;line-height:1.5}.ivu-alert-with-desc.ivu-alert-with-icon{padding:16px 16px 16px 69px}.ivu-alert-with-desc .ivu-alert-desc{display:block}.ivu-alert-with-desc .ivu-alert-message{font-size:14px;color:#17233d;display:block}.ivu-alert-with-desc .ivu-alert-icon{top:50%;left:24px;margin-top:-24px;font-size:28px}.ivu-alert-with-banner{border-radius:0}.ivu-collapse{background-color:#f7f7f7;border-radius:3px;border:1px solid #dcdee2}.ivu-collapse-simple{border-left:none;border-right:none;background-color:#fff;border-radius:0}.ivu-collapse>.ivu-collapse-item{border-top:1px solid #dcdee2}.ivu-collapse>.ivu-collapse-item:first-child{border-top:0}.ivu-collapse>.ivu-collapse-item>.ivu-collapse-header{height:38px;line-height:38px;padding-left:16px;color:#666;cursor:pointer;position:relative;border-bottom:1px solid transparent;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-collapse>.ivu-collapse-item>.ivu-collapse-header>i{-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out;margin-right:14px}.ivu-collapse>.ivu-collapse-item.ivu-collapse-item-active>.ivu-collapse-header{border-bottom:1px solid #dcdee2}.ivu-collapse-simple>.ivu-collapse-item.ivu-collapse-item-active>.ivu-collapse-header{border-bottom:1px solid transparent}.ivu-collapse>.ivu-collapse-item.ivu-collapse-item-active>.ivu-collapse-header>i{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.ivu-collapse-content{color:#515a6e;padding:0 16px;background-color:#fff}.ivu-collapse-content>.ivu-collapse-content-box{padding-top:16px;padding-bottom:16px}.ivu-collapse-simple>.ivu-collapse-item>.ivu-collapse-content>.ivu-collapse-content-box{padding-top:0}.ivu-collapse-item:last-child>.ivu-collapse-content{border-radius:0 0 3px 3px}.ivu-card{background:#fff;border-radius:4px;font-size:14px;position:relative;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-card-bordered{border:1px solid #dcdee2;border-color:#e8eaec}.ivu-card-shadow{-webkit-box-shadow:0 1px 1px 0 rgba(0,0,0,.1);box-shadow:0 1px 1px 0 rgba(0,0,0,.1)}.ivu-card:hover{-webkit-box-shadow:0 1px 6px rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.2);border-color:#eee}.ivu-card.ivu-card-dis-hover:hover{-webkit-box-shadow:none;box-shadow:none;border-color:transparent}.ivu-card.ivu-card-dis-hover.ivu-card-bordered:hover{border-color:#e8eaec}.ivu-card.ivu-card-shadow:hover{-webkit-box-shadow:0 1px 1px 0 rgba(0,0,0,.1);box-shadow:0 1px 1px 0 rgba(0,0,0,.1)}.ivu-card-head{border-bottom:1px solid #e8eaec;padding:14px 16px;line-height:1}.ivu-card-head p,.ivu-card-head-inner{display:inline-block;width:100%;height:20px;line-height:20px;font-size:14px;color:#17233d;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-card-head p i,.ivu-card-head p span{vertical-align:middle}.ivu-card-extra{position:absolute;right:16px;top:14px}.ivu-card-body{padding:16px}.ivu-message{font-size:14px;position:fixed;z-index:1010;width:100%;top:16px;left:0;pointer-events:none}.ivu-message-notice{padding:8px;text-align:center;-webkit-transition:height .3s ease-in-out,padding .3s ease-in-out;transition:height .3s ease-in-out,padding .3s ease-in-out}.ivu-message-notice:first-child{margin-top:-8px}.ivu-message-notice-close{position:absolute;right:4px;top:10px;color:#999;outline:0}.ivu-message-notice-close i.ivu-icon{font-size:22px;color:#999;-webkit-transition:color .2s ease;transition:color .2s ease;position:relative;top:-3px}.ivu-message-notice-close i.ivu-icon:hover{color:#444}.ivu-message-notice-content{display:inline-block;pointer-events:all;padding:8px 16px;border-radius:4px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.2);background:#fff;position:relative}.ivu-message-notice-content-text{display:inline-block}.ivu-message-notice-closable .ivu-message-notice-content-text{padding-right:32px}.ivu-message-success .ivu-icon{color:#19be6b}.ivu-message-error .ivu-icon{color:#ed4014}.ivu-message-warning .ivu-icon{color:#f90}.ivu-message-info .ivu-icon,.ivu-message-loading .ivu-icon{color:#2d8cf0}.ivu-message .ivu-icon{margin-right:4px;font-size:16px;vertical-align:middle}.ivu-message-custom-content span{vertical-align:middle}.ivu-notice{width:335px;margin-right:24px;position:fixed;z-index:1010}.ivu-notice-content-with-icon{margin-left:51px}.ivu-notice-with-desc.ivu-notice-with-icon .ivu-notice-title{margin-left:51px}.ivu-notice-notice{margin-bottom:10px;padding:16px;border-radius:4px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.2);background:#fff;line-height:1;position:relative;overflow:hidden}.ivu-notice-notice-close{position:absolute;right:8px;top:15px;color:#999;outline:0}.ivu-notice-notice-close i{font-size:22px;color:#999;-webkit-transition:color .2s ease;transition:color .2s ease;position:relative;top:-3px}.ivu-notice-notice-close i:hover{color:#444}.ivu-notice-notice-content-with-render .ivu-notice-desc{display:none}.ivu-notice-notice-with-desc .ivu-notice-notice-close{top:11px}.ivu-notice-content-with-render-notitle{margin-left:26px}.ivu-notice-title{font-size:14px;line-height:17px;color:#17233d;padding-right:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-notice-with-desc .ivu-notice-title{font-weight:700;margin-bottom:8px}.ivu-notice-desc{font-size:12px;color:#515a6e;text-align:justify;line-height:1.5}.ivu-notice-with-desc.ivu-notice-with-icon .ivu-notice-desc{margin-left:51px}.ivu-notice-with-icon .ivu-notice-title{margin-left:26px}.ivu-notice-icon{position:absolute;top:-2px;font-size:16px}.ivu-notice-icon-success{color:#19be6b}.ivu-notice-icon-info{color:#2d8cf0}.ivu-notice-icon-warning{color:#f90}.ivu-notice-icon-error{color:#ed4014}.ivu-notice-with-desc .ivu-notice-icon{font-size:36px;top:-6px}.ivu-notice-custom-content{position:relative}.ivu-radio-focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2);z-index:1}.ivu-radio-group{display:inline-block;font-size:12px;vertical-align:middle}.ivu-radio-group-vertical .ivu-radio-wrapper{display:block;height:30px;line-height:30px}.ivu-radio-wrapper{font-size:12px;vertical-align:middle;display:inline-block;position:relative;white-space:nowrap;margin-right:8px;cursor:pointer}.ivu-radio-wrapper-disabled{cursor:not-allowed}.ivu-radio{display:inline-block;margin-right:4px;white-space:nowrap;position:relative;line-height:1;vertical-align:middle;cursor:pointer}.ivu-radio:hover .ivu-radio-inner{border-color:#bcbcbc}.ivu-radio-inner{display:inline-block;width:14px;height:14px;position:relative;top:0;left:0;background-color:#fff;border:1px solid #dcdee2;border-radius:50%;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-radio-inner:after{position:absolute;width:8px;height:8px;left:2px;top:2px;border-radius:6px;display:table;border-top:0;border-left:0;content:' ';background-color:#2d8cf0;opacity:0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0)}.ivu-radio-large{font-size:14px}.ivu-radio-large .ivu-radio-inner{width:16px;height:16px}.ivu-radio-large .ivu-radio-inner:after{width:10px;height:10px}.ivu-radio-large .ivu-radio-wrapper,.ivu-radio-large.ivu-radio-wrapper{font-size:14px}.ivu-radio-small .ivu-radio-inner{width:12px;height:12px}.ivu-radio-small .ivu-radio-inner:after{width:6px;height:6px}.ivu-radio-input{position:absolute;top:0;bottom:0;left:0;right:0;z-index:1;opacity:0;cursor:pointer}.ivu-radio-checked .ivu-radio-inner{border-color:#2d8cf0}.ivu-radio-checked .ivu-radio-inner:after{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-radio-checked:hover .ivu-radio-inner{border-color:#2d8cf0}.ivu-radio-disabled{cursor:not-allowed}.ivu-radio-disabled .ivu-radio-input{cursor:not-allowed}.ivu-radio-disabled:hover .ivu-radio-inner{border-color:#dcdee2}.ivu-radio-disabled .ivu-radio-inner{border-color:#dcdee2;background-color:#f3f3f3}.ivu-radio-disabled .ivu-radio-inner:after{background-color:#ccc}.ivu-radio-disabled .ivu-radio-disabled+span{color:#ccc}span.ivu-radio+*{margin-left:2px;margin-right:2px}.ivu-radio-group-button{font-size:0;-webkit-text-size-adjust:none}.ivu-radio-group-button .ivu-radio{width:0;margin-right:0}.ivu-radio-group-button .ivu-radio-wrapper{display:inline-block;height:32px;line-height:30px;margin:0;padding:0 15px;font-size:12px;color:#515a6e;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;cursor:pointer;border:1px solid #dcdee2;border-left:0;background:#fff;position:relative}.ivu-radio-group-button .ivu-radio-wrapper>span{margin-left:0}.ivu-radio-group-button .ivu-radio-wrapper:after,.ivu-radio-group-button .ivu-radio-wrapper:before{content:'';display:block;position:absolute;width:1px;height:100%;left:-1px;top:0;background:#dcdee2;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-radio-group-button .ivu-radio-wrapper:after{height:36px;left:-1px;top:-3px;background:rgba(45,140,240,.2);opacity:0}.ivu-radio-group-button .ivu-radio-wrapper:first-child{border-radius:4px 0 0 4px;border-left:1px solid #dcdee2}.ivu-radio-group-button .ivu-radio-wrapper:first-child:after,.ivu-radio-group-button .ivu-radio-wrapper:first-child:before{display:none}.ivu-radio-group-button .ivu-radio-wrapper:last-child{border-radius:0 4px 4px 0}.ivu-radio-group-button .ivu-radio-wrapper:first-child:last-child{border-radius:4px}.ivu-radio-group-button .ivu-radio-wrapper:hover{position:relative;color:#2d8cf0}.ivu-radio-group-button .ivu-radio-wrapper:hover .ivu-radio{background-color:#000}.ivu-radio-group-button .ivu-radio-wrapper .ivu-radio-inner,.ivu-radio-group-button .ivu-radio-wrapper input{opacity:0;width:0;height:0}.ivu-radio-group-button .ivu-radio-wrapper-checked{background:#fff;border-color:#2d8cf0;color:#2d8cf0;-webkit-box-shadow:-1px 0 0 0 #2d8cf0;box-shadow:-1px 0 0 0 #2d8cf0;z-index:1}.ivu-radio-group-button .ivu-radio-wrapper-checked:before{background:#2d8cf0;opacity:.1}.ivu-radio-group-button .ivu-radio-wrapper-checked.ivu-radio-focus{-webkit-box-shadow:-1px 0 0 0 #2d8cf0,0 0 0 2px rgba(45,140,240,.2);box-shadow:-1px 0 0 0 #2d8cf0,0 0 0 2px rgba(45,140,240,.2);-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-radio-group-button .ivu-radio-wrapper-checked.ivu-radio-focus:after{left:-3px;top:-3px;opacity:1;background:rgba(45,140,240,.2)}.ivu-radio-group-button .ivu-radio-wrapper-checked.ivu-radio-focus:first-child{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-radio-group-button .ivu-radio-wrapper-checked:first-child{border-color:#2d8cf0;-webkit-box-shadow:none;box-shadow:none}.ivu-radio-group-button .ivu-radio-wrapper-checked:hover{border-color:#57a3f3;color:#57a3f3}.ivu-radio-group-button .ivu-radio-wrapper-checked:active{border-color:#2b85e4;color:#2b85e4}.ivu-radio-group-button .ivu-radio-wrapper-disabled{border-color:#dcdee2;background-color:#f7f7f7;cursor:not-allowed;color:#ccc}.ivu-radio-group-button .ivu-radio-wrapper-disabled:first-child,.ivu-radio-group-button .ivu-radio-wrapper-disabled:hover{border-color:#dcdee2;background-color:#f7f7f7;color:#ccc}.ivu-radio-group-button .ivu-radio-wrapper-disabled:first-child{border-left-color:#dcdee2}.ivu-radio-group-button .ivu-radio-wrapper-disabled.ivu-radio-wrapper-checked{color:#fff;background-color:#e6e6e6;border-color:#dcdee2;-webkit-box-shadow:none!important;box-shadow:none!important}.ivu-radio-group-button.ivu-radio-group-large .ivu-radio-wrapper{height:36px;line-height:34px;font-size:14px}.ivu-radio-group-button.ivu-radio-group-large .ivu-radio-wrapper:after{height:40px}.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper{height:24px;line-height:22px;padding:0 12px;font-size:12px}.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper:after{height:28px}.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper:first-child{border-radius:3px 0 0 3px}.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper:last-child{border-radius:0 3px 3px 0}.ivu-checkbox-focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2);z-index:1}.ivu-checkbox{display:inline-block;vertical-align:middle;white-space:nowrap;cursor:pointer;line-height:1;position:relative}.ivu-checkbox-disabled{cursor:not-allowed}.ivu-checkbox:hover .ivu-checkbox-inner{border-color:#bcbcbc}.ivu-checkbox-inner{display:inline-block;width:14px;height:14px;position:relative;top:0;left:0;border:1px solid #dcdee2;border-radius:2px;background-color:#fff;-webkit-transition:border-color .2s ease-in-out,background-color .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border-color .2s ease-in-out,background-color .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border-color .2s ease-in-out,background-color .2s ease-in-out,box-shadow .2s ease-in-out;transition:border-color .2s ease-in-out,background-color .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-checkbox-inner:after{content:'';display:table;width:4px;height:8px;position:absolute;top:1px;left:4px;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(0);-ms-transform:rotate(45deg) scale(0);transform:rotate(45deg) scale(0);-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-checkbox-large .ivu-checkbox-inner{width:16px;height:16px}.ivu-checkbox-large .ivu-checkbox-inner:after{width:5px;height:9px}.ivu-checkbox-small{font-size:12px}.ivu-checkbox-small .ivu-checkbox-inner{width:12px;height:12px}.ivu-checkbox-small .ivu-checkbox-inner:after{top:0;left:3px}.ivu-checkbox-input{width:100%;height:100%;position:absolute;top:0;bottom:0;left:0;right:0;z-index:1;cursor:pointer;opacity:0}.ivu-checkbox-input[disabled]{cursor:not-allowed}.ivu-checkbox-checked:hover .ivu-checkbox-inner{border-color:#2d8cf0}.ivu-checkbox-checked .ivu-checkbox-inner{border-color:#2d8cf0;background-color:#2d8cf0}.ivu-checkbox-checked .ivu-checkbox-inner:after{content:'';display:table;width:4px;height:8px;position:absolute;top:1px;left:4px;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(1);-ms-transform:rotate(45deg) scale(1);transform:rotate(45deg) scale(1);-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-checkbox-large .ivu-checkbox-checked .ivu-checkbox-inner:after{width:5px;height:9px}.ivu-checkbox-small .ivu-checkbox-checked .ivu-checkbox-inner:after{top:0;left:3px}.ivu-checkbox-disabled.ivu-checkbox-checked:hover .ivu-checkbox-inner{border-color:#dcdee2}.ivu-checkbox-disabled.ivu-checkbox-checked .ivu-checkbox-inner{background-color:#f3f3f3;border-color:#dcdee2}.ivu-checkbox-disabled.ivu-checkbox-checked .ivu-checkbox-inner:after{-webkit-animation-name:none;animation-name:none;border-color:#ccc}.ivu-checkbox-disabled:hover .ivu-checkbox-inner{border-color:#dcdee2}.ivu-checkbox-disabled .ivu-checkbox-inner{border-color:#dcdee2;background-color:#f3f3f3}.ivu-checkbox-disabled .ivu-checkbox-inner:after{-webkit-animation-name:none;animation-name:none;border-color:#f3f3f3}.ivu-checkbox-disabled .ivu-checkbox-inner-input{cursor:default}.ivu-checkbox-disabled+span{color:#ccc;cursor:not-allowed}.ivu-checkbox-indeterminate .ivu-checkbox-inner:after{content:'';width:8px;height:1px;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);position:absolute;left:2px;top:5px}.ivu-checkbox-indeterminate:hover .ivu-checkbox-inner{border-color:#2d8cf0}.ivu-checkbox-indeterminate .ivu-checkbox-inner{background-color:#2d8cf0;border-color:#2d8cf0}.ivu-checkbox-indeterminate.ivu-checkbox-disabled .ivu-checkbox-inner{background-color:#f3f3f3;border-color:#dcdee2}.ivu-checkbox-indeterminate.ivu-checkbox-disabled .ivu-checkbox-inner:after{border-color:#c5c8ce}.ivu-checkbox-large .ivu-checkbox-indeterminate .ivu-checkbox-inner:after{width:10px;top:6px}.ivu-checkbox-small .ivu-checkbox-indeterminate .ivu-checkbox-inner:after{width:6px;top:4px}.ivu-checkbox-wrapper{cursor:pointer;font-size:12px;display:inline-block;margin-right:8px}.ivu-checkbox-wrapper-disabled{cursor:not-allowed}.ivu-checkbox-wrapper.ivu-checkbox-large{font-size:14px}.ivu-checkbox+span,.ivu-checkbox-wrapper+span{margin-right:4px}.ivu-checkbox-group{font-size:14px}.ivu-checkbox-group-item{display:inline-block}.ivu-switch{display:inline-block;width:44px;height:22px;line-height:20px;border-radius:22px;vertical-align:middle;border:1px solid #ccc;background-color:#ccc;position:relative;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-switch-loading{opacity:.4}.ivu-switch-inner{color:#fff;font-size:12px;position:absolute;left:23px}.ivu-switch-inner i{width:12px;height:12px;text-align:center;position:relative;top:-1px}.ivu-switch:after{content:'';width:18px;height:18px;border-radius:18px;background-color:#fff;position:absolute;left:1px;top:1px;cursor:pointer;-webkit-transition:left .2s ease-in-out,width .2s ease-in-out;transition:left .2s ease-in-out,width .2s ease-in-out}.ivu-switch:active:after{width:26px}.ivu-switch:before{content:'';display:none;width:14px;height:14px;border-radius:50%;background-color:transparent;position:absolute;left:3px;top:3px;z-index:1;border:1px solid #2d8cf0;border-color:transparent transparent transparent #2d8cf0;-webkit-animation:switch-loading 1s linear;animation:switch-loading 1s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.ivu-switch-loading:before{display:block}.ivu-switch:focus{-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2);outline:0}.ivu-switch:focus:hover{-webkit-box-shadow:none;box-shadow:none}.ivu-switch-small{width:28px;height:16px;line-height:14px}.ivu-switch-small:after{width:12px;height:12px}.ivu-switch-small:active:after{width:14px}.ivu-switch-small:before{width:10px;height:10px;left:2px;top:2px}.ivu-switch-small.ivu-switch-checked:after{left:13px}.ivu-switch-small.ivu-switch-checked:before{left:14px}.ivu-switch-small:active.ivu-switch-checked:after{left:11px}.ivu-switch-large{width:56px}.ivu-switch-large:active:after{width:26px}.ivu-switch-large:active:after{width:30px}.ivu-switch-large.ivu-switch-checked:after{left:35px}.ivu-switch-large.ivu-switch-checked:before{left:37px}.ivu-switch-large:active.ivu-switch-checked:after{left:23px}.ivu-switch-checked{border-color:#2d8cf0;background-color:#2d8cf0}.ivu-switch-checked .ivu-switch-inner{left:7px}.ivu-switch-checked:after{left:23px}.ivu-switch-checked:before{left:25px}.ivu-switch-checked:active:after{left:15px}.ivu-switch-disabled{cursor:not-allowed;opacity:.4}.ivu-switch-disabled:after{background:#fff;cursor:not-allowed}.ivu-switch-disabled .ivu-switch-inner{color:#fff}.ivu-switch-disabled.ivu-switch-checked{border-color:#2d8cf0;background-color:#2d8cf0;opacity:.4}.ivu-switch-disabled.ivu-switch-checked:after{background:#fff}.ivu-switch-disabled.ivu-switch-checked .ivu-switch-inner{color:#fff}@-webkit-keyframes switch-loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes switch-loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ivu-input-number{display:inline-block;width:100%;line-height:1.5;padding:4px 7px;font-size:12px;color:#515a6e;background-color:#fff;background-image:none;position:relative;cursor:text;-webkit-transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;margin:0;padding:0;width:80px;height:32px;line-height:32px;vertical-align:middle;border:1px solid #dcdee2;border-radius:4px;overflow:hidden}.ivu-input-number::-moz-placeholder{color:#c5c8ce;opacity:1}.ivu-input-number:-ms-input-placeholder{color:#c5c8ce}.ivu-input-number::-webkit-input-placeholder{color:#c5c8ce}.ivu-input-number:hover{border-color:#57a3f3}.ivu-input-number:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-input-number[disabled],fieldset[disabled] .ivu-input-number{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-input-number[disabled]:hover,fieldset[disabled] .ivu-input-number:hover{border-color:#e3e5e8}textarea.ivu-input-number{max-width:100%;height:auto;min-height:32px;vertical-align:bottom;font-size:14px}.ivu-input-number-large{font-size:14px;padding:6px 7px;height:36px}.ivu-input-number-small{padding:1px 7px;height:24px;border-radius:3px}.ivu-input-number-handler-wrap{width:22px;height:100%;border-left:1px solid #dcdee2;border-radius:0 4px 4px 0;background:#fff;position:absolute;top:0;right:0;opacity:0;-webkit-transition:opacity .2s ease-in-out;transition:opacity .2s ease-in-out}.ivu-input-number:hover .ivu-input-number-handler-wrap{opacity:1}.ivu-input-number-handler-up{cursor:pointer}.ivu-input-number-handler-up-inner{top:1px}.ivu-input-number-handler-down{border-top:1px solid #dcdee2;top:-1px;cursor:pointer}.ivu-input-number-handler{display:block;width:100%;height:16px;line-height:0;text-align:center;overflow:hidden;color:#999;position:relative}.ivu-input-number-handler:hover .ivu-input-number-handler-down-inner,.ivu-input-number-handler:hover .ivu-input-number-handler-up-inner{color:#57a3f3}.ivu-input-number-handler-down-inner,.ivu-input-number-handler-up-inner{width:12px;height:12px;line-height:12px;font-size:14px;color:#999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:absolute;right:5px;-webkit-transition:all .2s linear;transition:all .2s linear}.ivu-input-number:hover{border-color:#57a3f3}.ivu-input-number-focused{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-input-number-disabled{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-input-number-disabled:hover{border-color:#e3e5e8}.ivu-input-number-input-wrap{overflow:hidden;height:32px}.ivu-input-number-input{width:100%;height:32px;line-height:32px;padding:0 7px;text-align:left;outline:0;-moz-appearance:textfield;color:#666;border:0;border-radius:4px;-webkit-transition:all .2s linear;transition:all .2s linear}.ivu-input-number-input[disabled]{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-input-number-input[disabled]:hover{border-color:#e3e5e8}.ivu-input-number-input::-webkit-input-placeholder{color:#c5c8ce}.ivu-input-number-input::-ms-input-placeholder{color:#c5c8ce}.ivu-input-number-input::placeholder{color:#c5c8ce}.ivu-input-number-large{padding:0}.ivu-input-number-large .ivu-input-number-input-wrap{height:36px}.ivu-input-number-large .ivu-input-number-handler{height:18px}.ivu-input-number-large input{height:36px;line-height:36px}.ivu-input-number-large .ivu-input-number-handler-up-inner{top:2px}.ivu-input-number-large .ivu-input-number-handler-down-inner{bottom:2px}.ivu-input-number-small{padding:0}.ivu-input-number-small .ivu-input-number-input-wrap{height:24px}.ivu-input-number-small .ivu-input-number-handler{height:12px}.ivu-input-number-small input{height:24px;line-height:24px;margin-top:-1px;vertical-align:top}.ivu-input-number-small .ivu-input-number-handler-up-inner{top:-1px}.ivu-input-number-small .ivu-input-number-handler-down-inner{bottom:-1px}.ivu-input-number-disabled .ivu-input-number-handler-down-inner,.ivu-input-number-disabled .ivu-input-number-handler-up-inner,.ivu-input-number-handler-down-disabled .ivu-input-number-handler-down-inner,.ivu-input-number-handler-down-disabled .ivu-input-number-handler-up-inner,.ivu-input-number-handler-up-disabled .ivu-input-number-handler-down-inner,.ivu-input-number-handler-up-disabled .ivu-input-number-handler-up-inner{opacity:.72;color:#ccc!important;cursor:not-allowed}.ivu-input-number-disabled .ivu-input-number-input{opacity:.72;cursor:not-allowed;background-color:#f3f3f3}.ivu-input-number-disabled .ivu-input-number-handler-wrap{display:none}.ivu-input-number-disabled .ivu-input-number-handler{opacity:.72;color:#ccc!important;cursor:not-allowed}.ivu-form-item-error .ivu-input-number{border:1px solid #ed4014}.ivu-form-item-error .ivu-input-number:hover{border-color:#ed4014}.ivu-form-item-error .ivu-input-number:focus{border-color:#ed4014;outline:0;-webkit-box-shadow:0 0 0 2px rgba(237,64,20,.2);box-shadow:0 0 0 2px rgba(237,64,20,.2)}.ivu-form-item-error .ivu-input-number-focused{border-color:#ed4014;outline:0;-webkit-box-shadow:0 0 0 2px rgba(237,64,20,.2);box-shadow:0 0 0 2px rgba(237,64,20,.2)}.ivu-scroll-wrapper{width:auto;margin:0 auto;position:relative;outline:0}.ivu-scroll-container{overflow-y:scroll}.ivu-scroll-content{opacity:1;-webkit-transition:opacity .5s;transition:opacity .5s}.ivu-scroll-content-loading{opacity:.5}.ivu-scroll-loader{text-align:center;padding:0;-webkit-transition:padding .5s;transition:padding .5s}.ivu-scroll-loader-wrapper{padding:5px 0;height:0;background-color:inherit;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);-webkit-transition:opacity .3s,height .5s,-webkit-transform .5s;transition:opacity .3s,height .5s,-webkit-transform .5s;transition:opacity .3s,transform .5s,height .5s;transition:opacity .3s,transform .5s,height .5s,-webkit-transform .5s}.ivu-scroll-loader-wrapper-active{height:40px;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}@-webkit-keyframes ani-demo-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes ani-demo-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ivu-scroll-loader-wrapper .ivu-scroll-spinner{position:relative}.ivu-scroll-loader-wrapper .ivu-scroll-spinner-icon{-webkit-animation:ani-demo-spin 1s linear infinite;animation:ani-demo-spin 1s linear infinite}.ivu-tag{display:inline-block;height:22px;line-height:22px;margin:2px 4px 2px 0;padding:0 8px;border:1px solid #e8eaec;border-radius:3px;background:#f7f7f7;font-size:12px;vertical-align:middle;opacity:1;overflow:hidden;cursor:pointer}.ivu-tag:not(.ivu-tag-border):not(.ivu-tag-dot):not(.ivu-tag-checked){background:0 0;border:0;color:#515a6e}.ivu-tag:not(.ivu-tag-border):not(.ivu-tag-dot):not(.ivu-tag-checked) .ivu-icon-ios-close{color:#515a6e!important}.ivu-tag-color-error{color:#ed4014!important;border-color:#ed4014}.ivu-tag-color-success{color:#19be6b!important;border-color:#19be6b}.ivu-tag-color-primary{color:#2d8cf0!important;border-color:#2d8cf0}.ivu-tag-color-warning{color:#f90!important;border-color:#f90}.ivu-tag-color-white{color:#fff!important}.ivu-tag-dot{height:32px;line-height:32px;border:1px solid #e8eaec!important;color:#515a6e!important;background:#fff!important;padding:0 12px}.ivu-tag-dot-inner{display:inline-block;width:12px;height:12px;margin-right:8px;border-radius:50%;background:#e8eaec;position:relative;top:1px}.ivu-tag-dot .ivu-icon-ios-close{color:#666!important;margin-left:12px!important}.ivu-tag-border{height:24px;line-height:24px;border:1px solid #e8eaec;color:#e8eaec;background:#fff!important;position:relative}.ivu-tag-border .ivu-icon-ios-close{color:#666;margin-left:12px!important}.ivu-tag-border:after{content:"";display:none;width:1px;background:currentColor;position:absolute;top:0;bottom:0;right:22px}.ivu-tag-border.ivu-tag-closable:after{display:block}.ivu-tag-border.ivu-tag-closable .ivu-icon-ios-close{margin-left:18px!important;left:4px;top:-1px}.ivu-tag-border.ivu-tag-primary{color:#2d8cf0!important;border:1px solid #2d8cf0!important}.ivu-tag-border.ivu-tag-primary:after{background:#2d8cf0}.ivu-tag-border.ivu-tag-primary .ivu-icon-ios-close{color:#2d8cf0!important}.ivu-tag-border.ivu-tag-success{color:#19be6b!important;border:1px solid #19be6b!important}.ivu-tag-border.ivu-tag-success:after{background:#19be6b}.ivu-tag-border.ivu-tag-success .ivu-icon-ios-close{color:#19be6b!important}.ivu-tag-border.ivu-tag-warning{color:#f90!important;border:1px solid #f90!important}.ivu-tag-border.ivu-tag-warning:after{background:#f90}.ivu-tag-border.ivu-tag-warning .ivu-icon-ios-close{color:#f90!important}.ivu-tag-border.ivu-tag-error{color:#ed4014!important;border:1px solid #ed4014!important}.ivu-tag-border.ivu-tag-error:after{background:#ed4014}.ivu-tag-border.ivu-tag-error .ivu-icon-ios-close{color:#ed4014!important}.ivu-tag:hover{opacity:.85}.ivu-tag-text{color:#515a6e}.ivu-tag-text a:first-child:last-child{display:inline-block;margin:0 -8px;padding:0 8px}.ivu-tag .ivu-icon-ios-close{display:inline-block;font-size:14px;-webkit-transform:scale(1.42857143) rotate(0);-ms-transform:scale(1.42857143) rotate(0);transform:scale(1.42857143) rotate(0);cursor:pointer;margin-left:2px;color:#666;opacity:.66;position:relative;top:-1px}:root .ivu-tag .ivu-icon-ios-close{font-size:14px}.ivu-tag .ivu-icon-ios-close:hover{opacity:1}.ivu-tag-error,.ivu-tag-primary,.ivu-tag-success,.ivu-tag-warning{border:0}.ivu-tag-error,.ivu-tag-error .ivu-icon-ios-close,.ivu-tag-error .ivu-icon-ios-close:hover,.ivu-tag-error a,.ivu-tag-error a:hover,.ivu-tag-primary,.ivu-tag-primary .ivu-icon-ios-close,.ivu-tag-primary .ivu-icon-ios-close:hover,.ivu-tag-primary a,.ivu-tag-primary a:hover,.ivu-tag-success,.ivu-tag-success .ivu-icon-ios-close,.ivu-tag-success .ivu-icon-ios-close:hover,.ivu-tag-success a,.ivu-tag-success a:hover,.ivu-tag-warning,.ivu-tag-warning .ivu-icon-ios-close,.ivu-tag-warning .ivu-icon-ios-close:hover,.ivu-tag-warning a,.ivu-tag-warning a:hover{color:#fff}.ivu-tag-primary,.ivu-tag-primary.ivu-tag-dot .ivu-tag-dot-inner{background:#2d8cf0}.ivu-tag-success,.ivu-tag-success.ivu-tag-dot .ivu-tag-dot-inner{background:#19be6b}.ivu-tag-warning,.ivu-tag-warning.ivu-tag-dot .ivu-tag-dot-inner{background:#f90}.ivu-tag-error,.ivu-tag-error.ivu-tag-dot .ivu-tag-dot-inner{background:#ed4014}.ivu-tag-pink{line-height:20px;background:#fff0f6;border-color:#ffadd2}.ivu-tag-pink .ivu-tag-text{color:#eb2f96!important}.ivu-tag-pink.ivu-tag-dot{line-height:32px}.ivu-tag-magenta{line-height:20px;background:#fff0f6;border-color:#ffadd2}.ivu-tag-magenta .ivu-tag-text{color:#eb2f96!important}.ivu-tag-magenta.ivu-tag-dot{line-height:32px}.ivu-tag-red{line-height:20px;background:#fff1f0;border-color:#ffa39e}.ivu-tag-red .ivu-tag-text{color:#f5222d!important}.ivu-tag-red.ivu-tag-dot{line-height:32px}.ivu-tag-volcano{line-height:20px;background:#fff2e8;border-color:#ffbb96}.ivu-tag-volcano .ivu-tag-text{color:#fa541c!important}.ivu-tag-volcano.ivu-tag-dot{line-height:32px}.ivu-tag-orange{line-height:20px;background:#fff7e6;border-color:#ffd591}.ivu-tag-orange .ivu-tag-text{color:#fa8c16!important}.ivu-tag-orange.ivu-tag-dot{line-height:32px}.ivu-tag-yellow{line-height:20px;background:#feffe6;border-color:#fffb8f}.ivu-tag-yellow .ivu-tag-text{color:#fadb14!important}.ivu-tag-yellow.ivu-tag-dot{line-height:32px}.ivu-tag-gold{line-height:20px;background:#fffbe6;border-color:#ffe58f}.ivu-tag-gold .ivu-tag-text{color:#faad14!important}.ivu-tag-gold.ivu-tag-dot{line-height:32px}.ivu-tag-cyan{line-height:20px;background:#e6fffb;border-color:#87e8de}.ivu-tag-cyan .ivu-tag-text{color:#13c2c2!important}.ivu-tag-cyan.ivu-tag-dot{line-height:32px}.ivu-tag-lime{line-height:20px;background:#fcffe6;border-color:#eaff8f}.ivu-tag-lime .ivu-tag-text{color:#a0d911!important}.ivu-tag-lime.ivu-tag-dot{line-height:32px}.ivu-tag-green{line-height:20px;background:#f6ffed;border-color:#b7eb8f}.ivu-tag-green .ivu-tag-text{color:#52c41a!important}.ivu-tag-green.ivu-tag-dot{line-height:32px}.ivu-tag-blue{line-height:20px;background:#e6f7ff;border-color:#91d5ff}.ivu-tag-blue .ivu-tag-text{color:#1890ff!important}.ivu-tag-blue.ivu-tag-dot{line-height:32px}.ivu-tag-geekblue{line-height:20px;background:#f0f5ff;border-color:#adc6ff}.ivu-tag-geekblue .ivu-tag-text{color:#2f54eb!important}.ivu-tag-geekblue.ivu-tag-dot{line-height:32px}.ivu-tag-purple{line-height:20px;background:#f9f0ff;border-color:#d3adf7}.ivu-tag-purple .ivu-tag-text{color:#722ed1!important}.ivu-tag-purple.ivu-tag-dot{line-height:32px}.ivu-layout{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-ms-flex:auto;flex:auto;background:#f5f7f9}.ivu-layout.ivu-layout-has-sider{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.ivu-layout.ivu-layout-has-sider>.ivu-layout,.ivu-layout.ivu-layout-has-sider>.ivu-layout-content{overflow-x:hidden}.ivu-layout-footer,.ivu-layout-header{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ivu-layout-header{background:#515a6e;padding:0 50px;height:64px;line-height:64px}.ivu-layout-sider{-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;position:relative;background:#515a6e;min-width:0}.ivu-layout-sider-children{height:100%;padding-top:.1px;margin-top:-.1px}.ivu-layout-sider-has-trigger{padding-bottom:48px}.ivu-layout-sider-trigger{position:fixed;bottom:0;text-align:center;cursor:pointer;height:48px;line-height:48px;color:#fff;background:#515a6e;z-index:1000;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-layout-sider-trigger .ivu-icon{font-size:16px}.ivu-layout-sider-trigger>*{-webkit-transition:all .2s;transition:all .2s}.ivu-layout-sider-trigger-collapsed .ivu-layout-sider-trigger-icon{-webkit-transform:rotateZ(180deg);-ms-transform:rotate(180deg);transform:rotateZ(180deg)}.ivu-layout-sider-zero-width>*{overflow:hidden}.ivu-layout-sider-zero-width-trigger{position:absolute;top:64px;right:-36px;text-align:center;width:36px;height:42px;line-height:42px;background:#515a6e;color:#fff;font-size:18px;border-radius:0 6px 6px 0;cursor:pointer;-webkit-transition:background .3s ease;transition:background .3s ease}.ivu-layout-sider-zero-width-trigger:hover{background:#626b7d}.ivu-layout-sider-zero-width-trigger.ivu-layout-sider-zero-width-trigger-left{right:0;left:-36px;border-radius:6px 0 0 6px}.ivu-layout-footer{background:#f5f7f9;padding:24px 50px;color:#515a6e;font-size:14px}.ivu-layout-content{-webkit-box-flex:1;-ms-flex:auto;flex:auto}.ivu-loading-bar{width:100%;position:fixed;top:0;left:0;right:0;z-index:2000}.ivu-loading-bar-inner{-webkit-transition:width .2s linear;transition:width .2s linear}.ivu-loading-bar-inner-color-primary{background-color:#2d8cf0}.ivu-loading-bar-inner-failed-color-error{background-color:#ed4014}.ivu-progress{display:inline-block;width:100%;font-size:12px;position:relative}.ivu-progress-vertical{height:100%;width:auto}.ivu-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0}.ivu-progress-show-info .ivu-progress-outer{padding-right:55px;margin-right:-55px}.ivu-progress-vertical .ivu-progress-outer{height:100%;width:auto}.ivu-progress-inner{display:inline-block;width:100%;background-color:#f3f3f3;border-radius:100px;vertical-align:middle;position:relative}.ivu-progress-vertical .ivu-progress-inner{height:100%;width:auto}.ivu-progress-vertical .ivu-progress-inner:after,.ivu-progress-vertical .ivu-progress-inner>*{display:inline-block;vertical-align:bottom}.ivu-progress-vertical .ivu-progress-inner:after{content:'';height:100%}.ivu-progress-bg{border-radius:100px;background-color:#2d8cf0;-webkit-transition:all .2s linear;transition:all .2s linear;position:relative}.ivu-progress-success-bg{border-radius:100px;background-color:#19be6b;-webkit-transition:all .2s linear;transition:all .2s linear;position:absolute;top:0;left:0}.ivu-progress-text{display:inline-block;margin-left:5px;text-align:left;font-size:1em;vertical-align:middle}.ivu-progress-active .ivu-progress-bg:before{content:'';opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;background:#fff;border-radius:10px;-webkit-animation:ivu-progress-active 2s ease-in-out infinite;animation:ivu-progress-active 2s ease-in-out infinite}.ivu-progress-vertical.ivu-progress-active .ivu-progress-bg:before{top:auto;-webkit-animation:ivu-progress-active-vertical 2s ease-in-out infinite;animation:ivu-progress-active-vertical 2s ease-in-out infinite}.ivu-progress-wrong .ivu-progress-bg{background-color:#ed4014}.ivu-progress-wrong .ivu-progress-text{color:#ed4014}.ivu-progress-success .ivu-progress-bg{background-color:#19be6b}.ivu-progress-success .ivu-progress-text{color:#19be6b}@-webkit-keyframes ivu-progress-active{0%{opacity:.3;width:0}100%{opacity:0;width:100%}}@keyframes ivu-progress-active{0%{opacity:.3;width:0}100%{opacity:0;width:100%}}@-webkit-keyframes ivu-progress-active-vertical{0%{opacity:.3;height:0}100%{opacity:0;height:100%}}@keyframes ivu-progress-active-vertical{0%{opacity:.3;height:0}100%{opacity:0;height:100%}}.ivu-timeline{list-style:none;margin:0;padding:0}.ivu-timeline-item{margin:0!important;padding:0 0 12px 0;list-style:none;position:relative}.ivu-timeline-item-tail{height:100%;border-left:1px solid #e8eaec;position:absolute;left:6px;top:0}.ivu-timeline-item-pending .ivu-timeline-item-tail{display:none}.ivu-timeline-item-head{width:13px;height:13px;background-color:#fff;border-radius:50%;border:1px solid transparent;position:absolute}.ivu-timeline-item-head-blue{border-color:#2d8cf0;color:#2d8cf0}.ivu-timeline-item-head-red{border-color:#ed4014;color:#ed4014}.ivu-timeline-item-head-green{border-color:#19be6b;color:#19be6b}.ivu-timeline-item-head-custom{width:40px;height:auto;margin-top:6px;padding:3px 0;text-align:center;line-height:1;border:0;border-radius:0;font-size:14px;position:absolute;left:-13px;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ivu-timeline-item-content{padding:1px 1px 10px 24px;font-size:12px;position:relative;top:-3px}.ivu-timeline-item:last-child .ivu-timeline-item-tail{display:none}.ivu-timeline.ivu-timeline-pending .ivu-timeline-item:nth-last-of-type(2) .ivu-timeline-item-tail{border-left:1px dotted #e8eaec}.ivu-timeline.ivu-timeline-pending .ivu-timeline-item:nth-last-of-type(2) .ivu-timeline-item-content{min-height:48px}.ivu-page:after{content:'';display:block;height:0;clear:both;overflow:hidden;visibility:hidden}.ivu-page-item{display:inline-block;vertical-align:middle;min-width:32px;height:32px;line-height:30px;margin-right:4px;text-align:center;list-style:none;background-color:#fff;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;font-family:Arial;font-weight:500;border:1px solid #dcdee2;border-radius:4px;-webkit-transition:border .2s ease-in-out,color .2s ease-in-out;transition:border .2s ease-in-out,color .2s ease-in-out}.ivu-page-item a{font-family:"Monospaced Number";margin:0 6px;text-decoration:none;color:#515a6e}.ivu-page-item:hover{border-color:#2d8cf0}.ivu-page-item:hover a{color:#2d8cf0}.ivu-page-item-active{border-color:#2d8cf0}.ivu-page-item-active a,.ivu-page-item-active:hover a{color:#2d8cf0}.ivu-page-item-jump-next:after,.ivu-page-item-jump-prev:after{content:"•••";display:block;letter-spacing:1px;color:#ccc;text-align:center}.ivu-page-item-jump-next i,.ivu-page-item-jump-prev i{display:none}.ivu-page-item-jump-next:hover:after,.ivu-page-item-jump-prev:hover:after{display:none}.ivu-page-item-jump-next:hover i,.ivu-page-item-jump-prev:hover i{display:inline}.ivu-page-item-jump-prev:hover i:after{content:"\F115";margin-left:-8px}.ivu-page-item-jump-next:hover i:after{content:"\F11F";margin-left:-8px}.ivu-page-prev{margin-right:4px}.ivu-page-item-jump-next,.ivu-page-item-jump-prev{margin-right:4px}.ivu-page-item-jump-next,.ivu-page-item-jump-prev,.ivu-page-next,.ivu-page-prev{display:inline-block;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;min-width:32px;height:32px;line-height:30px;list-style:none;text-align:center;cursor:pointer;color:#666;font-family:Arial;border:1px solid #dcdee2;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-page-item-jump-next,.ivu-page-item-jump-prev{border-color:transparent}.ivu-page-next,.ivu-page-prev{background-color:#fff}.ivu-page-next a,.ivu-page-prev a{color:#666;font-size:14px}.ivu-page-next:hover,.ivu-page-prev:hover{border-color:#2d8cf0}.ivu-page-next:hover a,.ivu-page-prev:hover a{color:#2d8cf0}.ivu-page-disabled{cursor:not-allowed}.ivu-page-disabled a{color:#ccc}.ivu-page-disabled:hover{border-color:#dcdee2}.ivu-page-disabled:hover a{color:#ccc;cursor:not-allowed}.ivu-page-options{display:inline-block;vertical-align:middle;margin-left:15px}.ivu-page-options-sizer{display:inline-block;margin-right:10px}.ivu-page-options-elevator{display:inline-block;vertical-align:middle;height:32px;line-height:32px}.ivu-page-options-elevator input{display:inline-block;width:100%;height:32px;line-height:1.5;padding:4px 7px;font-size:12px;border:1px solid #dcdee2;color:#515a6e;background-color:#fff;background-image:none;position:relative;cursor:text;-webkit-transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;border-radius:4px;margin:0 8px;width:50px}.ivu-page-options-elevator input::-moz-placeholder{color:#c5c8ce;opacity:1}.ivu-page-options-elevator input:-ms-input-placeholder{color:#c5c8ce}.ivu-page-options-elevator input::-webkit-input-placeholder{color:#c5c8ce}.ivu-page-options-elevator input:hover{border-color:#57a3f3}.ivu-page-options-elevator input:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-page-options-elevator input[disabled],fieldset[disabled] .ivu-page-options-elevator input{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-page-options-elevator input[disabled]:hover,fieldset[disabled] .ivu-page-options-elevator input:hover{border-color:#e3e5e8}textarea.ivu-page-options-elevator input{max-width:100%;height:auto;min-height:32px;vertical-align:bottom;font-size:14px}.ivu-page-options-elevator input-large{font-size:14px;padding:6px 7px;height:36px}.ivu-page-options-elevator input-small{padding:1px 7px;height:24px;border-radius:3px}.ivu-page-total{display:inline-block;height:32px;line-height:32px;margin-right:10px}.ivu-page-simple .ivu-page-next,.ivu-page-simple .ivu-page-prev{margin:0;border:0;height:24px;line-height:normal;font-size:18px}.ivu-page-simple .ivu-page-simple-pager{display:inline-block;margin-right:8px;vertical-align:middle}.ivu-page-simple .ivu-page-simple-pager input{width:30px;height:24px;margin:0 8px;padding:5px 8px;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:#fff;outline:0;border:1px solid #dcdee2;border-radius:4px;-webkit-transition:border-color .2s ease-in-out;transition:border-color .2s ease-in-out}.ivu-page-simple .ivu-page-simple-pager input:hover{border-color:#2d8cf0}.ivu-page-simple .ivu-page-simple-pager span{padding:0 8px 0 2px}.ivu-page-custom-text,.ivu-page-custom-text:hover{border-color:transparent}.ivu-page.mini .ivu-page-total{height:24px;line-height:24px}.ivu-page.mini .ivu-page-item{border:0;margin:0;min-width:24px;height:24px;line-height:24px;border-radius:3px}.ivu-page.mini .ivu-page-next,.ivu-page.mini .ivu-page-prev{margin:0;min-width:24px;height:24px;line-height:22px;border:0}.ivu-page.mini .ivu-page-next a i:after,.ivu-page.mini .ivu-page-prev a i:after{height:24px;line-height:24px}.ivu-page.mini .ivu-page-item-jump-next,.ivu-page.mini .ivu-page-item-jump-prev{height:24px;line-height:24px;border:none;margin-right:0}.ivu-page.mini .ivu-page-options{margin-left:8px}.ivu-page.mini .ivu-page-options-elevator{height:24px;line-height:24px}.ivu-page.mini .ivu-page-options-elevator input{padding:1px 7px;height:24px;border-radius:3px;width:44px}.ivu-steps{font-size:0;width:100%;line-height:1.5}.ivu-steps-item{display:inline-block;position:relative;vertical-align:top}.ivu-steps-item.ivu-steps-status-wait .ivu-steps-head-inner{background-color:#fff}.ivu-steps-item.ivu-steps-status-wait .ivu-steps-head-inner span,.ivu-steps-item.ivu-steps-status-wait .ivu-steps-head-inner>.ivu-steps-icon{color:#ccc}.ivu-steps-item.ivu-steps-status-wait .ivu-steps-title{color:#999}.ivu-steps-item.ivu-steps-status-wait .ivu-steps-content{color:#999}.ivu-steps-item.ivu-steps-status-wait .ivu-steps-tail>i{background-color:#e8eaec}.ivu-steps-item.ivu-steps-status-process .ivu-steps-head-inner{border-color:#2d8cf0;background-color:#2d8cf0}.ivu-steps-item.ivu-steps-status-process .ivu-steps-head-inner span,.ivu-steps-item.ivu-steps-status-process .ivu-steps-head-inner>.ivu-steps-icon{color:#fff}.ivu-steps-item.ivu-steps-status-process .ivu-steps-title{color:#666}.ivu-steps-item.ivu-steps-status-process .ivu-steps-content{color:#666}.ivu-steps-item.ivu-steps-status-process .ivu-steps-tail>i{background-color:#e8eaec}.ivu-steps-item.ivu-steps-status-finish .ivu-steps-head-inner{background-color:#fff;border-color:#2d8cf0}.ivu-steps-item.ivu-steps-status-finish .ivu-steps-head-inner span,.ivu-steps-item.ivu-steps-status-finish .ivu-steps-head-inner>.ivu-steps-icon{color:#2d8cf0}.ivu-steps-item.ivu-steps-status-finish .ivu-steps-tail>i:after{width:100%;background:#2d8cf0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;opacity:1}.ivu-steps-item.ivu-steps-status-finish .ivu-steps-title{color:#999}.ivu-steps-item.ivu-steps-status-finish .ivu-steps-content{color:#999}.ivu-steps-item.ivu-steps-status-error .ivu-steps-head-inner{background-color:#fff;border-color:#ed4014}.ivu-steps-item.ivu-steps-status-error .ivu-steps-head-inner>.ivu-steps-icon{color:#ed4014}.ivu-steps-item.ivu-steps-status-error .ivu-steps-title{color:#ed4014}.ivu-steps-item.ivu-steps-status-error .ivu-steps-content{color:#ed4014}.ivu-steps-item.ivu-steps-status-error .ivu-steps-tail>i{background-color:#e8eaec}.ivu-steps-item.ivu-steps-next-error .ivu-steps-tail>i,.ivu-steps-item.ivu-steps-next-error .ivu-steps-tail>i:after{background-color:#ed4014}.ivu-steps-item.ivu-steps-custom .ivu-steps-head-inner{background:0 0;border:0;width:auto;height:auto}.ivu-steps-item.ivu-steps-custom .ivu-steps-head-inner>.ivu-steps-icon{font-size:20px;top:2px;width:20px;height:20px}.ivu-steps-item.ivu-steps-custom.ivu-steps-status-process .ivu-steps-head-inner>.ivu-steps-icon{color:#2d8cf0}.ivu-steps-item:last-child .ivu-steps-tail{display:none}.ivu-steps .ivu-steps-head,.ivu-steps .ivu-steps-main{position:relative;display:inline-block;vertical-align:top}.ivu-steps .ivu-steps-head{background:#fff}.ivu-steps .ivu-steps-head-inner{display:block;width:26px;height:26px;line-height:24px;margin-right:8px;text-align:center;border:1px solid #ccc;border-radius:50%;font-size:14px;-webkit-transition:background-color .2s ease-in-out;transition:background-color .2s ease-in-out}.ivu-steps .ivu-steps-head-inner>.ivu-steps-icon{line-height:1;position:relative}.ivu-steps .ivu-steps-head-inner>.ivu-steps-icon.ivu-icon{font-size:24px}.ivu-steps .ivu-steps-head-inner>.ivu-steps-icon.ivu-icon-ios-checkmark-empty,.ivu-steps .ivu-steps-head-inner>.ivu-steps-icon.ivu-icon-ios-close-empty{font-weight:700}.ivu-steps .ivu-steps-main{margin-top:2.5px;display:inline}.ivu-steps .ivu-steps-custom .ivu-steps-title{margin-top:2.5px}.ivu-steps .ivu-steps-title{display:inline-block;margin-bottom:4px;padding-right:10px;font-size:14px;font-weight:700;color:#666;background:#fff}.ivu-steps .ivu-steps-title>a:first-child:last-child{color:#666}.ivu-steps .ivu-steps-item-last .ivu-steps-title{padding-right:0;width:100%}.ivu-steps .ivu-steps-content{font-size:12px;color:#999}.ivu-steps .ivu-steps-tail{width:100%;padding:0 10px;position:absolute;left:0;top:13px}.ivu-steps .ivu-steps-tail>i{display:inline-block;width:100%;height:1px;vertical-align:top;background:#e8eaec;border-radius:1px;position:relative}.ivu-steps .ivu-steps-tail>i:after{content:'';width:0;height:100%;background:#e8eaec;opacity:0;position:absolute;top:0}.ivu-steps.ivu-steps-small .ivu-steps-head-inner{width:18px;height:18px;line-height:16px;margin-right:10px;text-align:center;border-radius:50%;font-size:12px}.ivu-steps.ivu-steps-small .ivu-steps-head-inner>.ivu-steps-icon.ivu-icon{font-size:16px;top:0}.ivu-steps.ivu-steps-small .ivu-steps-main{margin-top:0}.ivu-steps.ivu-steps-small .ivu-steps-title{margin-bottom:4px;margin-top:0;color:#666;font-size:12px;font-weight:700}.ivu-steps.ivu-steps-small .ivu-steps-content{font-size:12px;color:#999;padding-left:30px}.ivu-steps.ivu-steps-small .ivu-steps-tail{top:8px;padding:0 8px}.ivu-steps.ivu-steps-small .ivu-steps-tail>i{height:1px;width:100%;border-radius:1px}.ivu-steps .ivu-steps-item.ivu-steps-custom .ivu-steps-head-inner,.ivu-steps.ivu-steps-small .ivu-steps-item.ivu-steps-custom .ivu-steps-head-inner{width:inherit;height:inherit;line-height:inherit;border-radius:0;border:0;background:0 0}.ivu-steps-vertical .ivu-steps-item{display:block}.ivu-steps-vertical .ivu-steps-tail{position:absolute;left:13px;top:0;height:100%;width:1px;padding:30px 0 4px 0}.ivu-steps-vertical .ivu-steps-tail>i{height:100%;width:1px}.ivu-steps-vertical .ivu-steps-tail>i:after{height:0;width:100%}.ivu-steps-vertical .ivu-steps-status-finish .ivu-steps-tail>i:after{height:100%}.ivu-steps-vertical .ivu-steps-head{float:left}.ivu-steps-vertical .ivu-steps-head-inner{margin-right:16px}.ivu-steps-vertical .ivu-steps-main{min-height:47px;overflow:hidden;display:block}.ivu-steps-vertical .ivu-steps-main .ivu-steps-title{line-height:26px}.ivu-steps-vertical .ivu-steps-main .ivu-steps-content{padding-bottom:12px;padding-left:0}.ivu-steps-vertical .ivu-steps-custom .ivu-steps-icon{left:4px}.ivu-steps-vertical.ivu-steps-small .ivu-steps-custom .ivu-steps-icon{left:0}.ivu-steps-vertical.ivu-steps-small .ivu-steps-tail{position:absolute;left:9px;top:0;padding:22px 0 4px 0}.ivu-steps-vertical.ivu-steps-small .ivu-steps-tail>i{height:100%}.ivu-steps-vertical.ivu-steps-small .ivu-steps-title{line-height:18px}.ivu-steps-horizontal.ivu-steps-hidden{visibility:hidden}.ivu-steps-horizontal .ivu-steps-content{padding-left:35px}.ivu-steps-horizontal .ivu-steps-item:not(:first-child) .ivu-steps-head{padding-left:10px;margin-left:-10px}.ivu-modal{width:auto;margin:0 auto;position:relative;outline:0;top:100px}.ivu-modal-hidden{display:none!important}.ivu-modal-wrap{position:fixed;overflow:auto;top:0;right:0;bottom:0;left:0;z-index:1000;-webkit-overflow-scrolling:touch;outline:0}.ivu-modal-wrap *{-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-tap-highlight-color:transparent}.ivu-modal-mask{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(55,55,55,.6);height:100%;z-index:1000}.ivu-modal-mask-hidden{display:none}.ivu-modal-content{position:relative;background-color:#fff;border:0;border-radius:6px;background-clip:padding-box;-webkit-box-shadow:0 4px 12px rgba(0,0,0,.15);box-shadow:0 4px 12px rgba(0,0,0,.15)}.ivu-modal-content-no-mask{pointer-events:auto}.ivu-modal-content-drag{position:absolute}.ivu-modal-content-drag .ivu-modal-header{cursor:move}.ivu-modal-content-dragging{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-modal-header{border-bottom:1px solid #e8eaec;padding:14px 16px;line-height:1}.ivu-modal-header p,.ivu-modal-header-inner{display:inline-block;width:100%;height:20px;line-height:20px;font-size:14px;color:#17233d;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-modal-header p i,.ivu-modal-header p span{vertical-align:middle}.ivu-modal-close{z-index:1;font-size:12px;position:absolute;right:8px;top:8px;overflow:hidden;cursor:pointer}.ivu-modal-close .ivu-icon-ios-close{font-size:31px;color:#999;-webkit-transition:color .2s ease;transition:color .2s ease;position:relative;top:1px}.ivu-modal-close .ivu-icon-ios-close:hover{color:#444}.ivu-modal-body{padding:16px;font-size:12px;line-height:1.5}.ivu-modal-footer{border-top:1px solid #e8eaec;padding:12px 18px 12px 18px;text-align:right}.ivu-modal-footer button+button{margin-left:8px;margin-bottom:0}.ivu-modal-fullscreen{width:100%!important;top:0;bottom:0;position:absolute}.ivu-modal-fullscreen .ivu-modal-content{width:100%;border-radius:0;position:absolute;top:0;bottom:0}.ivu-modal-fullscreen .ivu-modal-body{width:100%;overflow:auto;position:absolute;top:51px;bottom:61px}.ivu-modal-fullscreen-no-header .ivu-modal-body{top:0}.ivu-modal-fullscreen-no-footer .ivu-modal-body{bottom:0}.ivu-modal-fullscreen .ivu-modal-footer{position:absolute;width:100%;bottom:0}.ivu-modal-no-mask{pointer-events:none}@media (max-width:576px){.ivu-modal{width:auto!important;margin:10px}.ivu-modal-fullscreen{width:100%!important;margin:0}.vertical-center-modal .ivu-modal{-webkit-box-flex:1;-ms-flex:1;flex:1}}.ivu-modal-confirm{padding:0 4px}.ivu-modal-confirm-head{padding:0 12px 0 0}.ivu-modal-confirm-head-icon{display:inline-block;font-size:28px;vertical-align:middle;position:relative;top:-2px}.ivu-modal-confirm-head-icon-info{color:#2d8cf0}.ivu-modal-confirm-head-icon-success{color:#19be6b}.ivu-modal-confirm-head-icon-warning{color:#f90}.ivu-modal-confirm-head-icon-error{color:#ed4014}.ivu-modal-confirm-head-icon-confirm{color:#f90}.ivu-modal-confirm-head-title{display:inline-block;vertical-align:middle;margin-left:12px;font-size:16px;color:#17233d;font-weight:700}.ivu-modal-confirm-body{padding-left:42px;font-size:14px;color:#515a6e;position:relative}.ivu-modal-confirm-body-render{margin:0;padding:0}.ivu-modal-confirm-footer{margin-top:20px;text-align:right}.ivu-modal-confirm-footer button+button{margin-left:8px;margin-bottom:0}.ivu-select{display:inline-block;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;vertical-align:middle;color:#515a6e;font-size:14px;line-height:normal}.ivu-select-selection{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;position:relative;background-color:#fff;border-radius:4px;border:1px solid #dcdee2;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-select-selection-focused,.ivu-select-selection:hover{border-color:#57a3f3}.ivu-select-selection-focused .ivu-select-arrow,.ivu-select-selection:hover .ivu-select-arrow{display:inline-block}.ivu-select-arrow{position:absolute;top:50%;right:8px;line-height:1;margin-top:-7px;font-size:14px;color:#808695;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-select-visible .ivu-select-selection{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-select-visible .ivu-select-arrow{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg);display:inline-block}.ivu-select-disabled .ivu-select-selection{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-select-disabled .ivu-select-selection:hover{border-color:#e3e5e8}.ivu-select-disabled .ivu-select-selection .ivu-select-arrow{display:none}.ivu-select-disabled .ivu-select-selection:hover{border-color:#dcdee2;-webkit-box-shadow:none;box-shadow:none}.ivu-select-disabled .ivu-select-selection:hover .ivu-select-arrow{display:inline-block}.ivu-select-single .ivu-select-selection{height:32px;position:relative}.ivu-select-single .ivu-select-selection .ivu-select-placeholder{color:#c5c8ce}.ivu-select-single .ivu-select-selection .ivu-select-placeholder,.ivu-select-single .ivu-select-selection .ivu-select-selected-value{display:block;height:30px;line-height:30px;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:8px;padding-right:24px}.ivu-select-multiple .ivu-select-selection{padding:0 24px 0 4px}.ivu-select-multiple .ivu-select-selection .ivu-select-placeholder{display:block;height:30px;line-height:30px;color:#c5c8ce;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:4px;padding-right:22px}.ivu-select-large.ivu-select-single .ivu-select-selection{height:36px}.ivu-select-large.ivu-select-single .ivu-select-selection .ivu-select-placeholder,.ivu-select-large.ivu-select-single .ivu-select-selection .ivu-select-selected-value{height:34px;line-height:34px;font-size:14px}.ivu-select-large.ivu-select-multiple .ivu-select-selection{min-height:36px}.ivu-select-large.ivu-select-multiple .ivu-select-selection .ivu-select-placeholder,.ivu-select-large.ivu-select-multiple .ivu-select-selection .ivu-select-selected-value{min-height:34px;line-height:34px;font-size:14px}.ivu-select-small.ivu-select-single .ivu-select-selection{height:24px;border-radius:3px}.ivu-select-small.ivu-select-single .ivu-select-selection .ivu-select-placeholder,.ivu-select-small.ivu-select-single .ivu-select-selection .ivu-select-selected-value{height:22px;line-height:22px}.ivu-select-small.ivu-select-multiple .ivu-select-selection{min-height:24px;border-radius:3px}.ivu-select-small.ivu-select-multiple .ivu-select-selection .ivu-select-placeholder,.ivu-select-small.ivu-select-multiple .ivu-select-selection .ivu-select-selected-value{height:auto;min-height:22px;line-height:22px}.ivu-select-input{display:inline-block;height:32px;line-height:32px;padding:0 24px 0 8px;font-size:12px;outline:0;border:none;-webkit-box-sizing:border-box;box-sizing:border-box;color:#515a6e;background-color:transparent;position:relative;cursor:pointer}.ivu-select-input::-moz-placeholder{color:#c5c8ce;opacity:1}.ivu-select-input:-ms-input-placeholder{color:#c5c8ce}.ivu-select-input::-webkit-input-placeholder{color:#c5c8ce}.ivu-select-input[disabled]{cursor:not-allowed;color:#ccc;-webkit-text-fill-color:#ccc}.ivu-select-single .ivu-select-input{width:100%}.ivu-select-large .ivu-select-input{font-size:14px;height:36px}.ivu-select-small .ivu-select-input{height:22px;line-height:22px}.ivu-select-multiple .ivu-select-input{height:29px;line-height:32px;padding:0 0 0 4px}.ivu-select-not-found{text-align:center;color:#c5c8ce}.ivu-select-not-found li:not([class^=ivu-]){margin-bottom:0}.ivu-select-loading{text-align:center;color:#c5c8ce}.ivu-select-multiple .ivu-tag{height:24px;line-height:22px;margin:3px 4px 3px 0;max-width:99%;position:relative}.ivu-select-multiple .ivu-tag span{display:block;margin-right:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-select-multiple .ivu-tag i{display:block;position:absolute;right:4px;top:4px}.ivu-select-large.ivu-select-multiple .ivu-tag{height:28px;line-height:26px;font-size:14px}.ivu-select-large.ivu-select-multiple .ivu-tag i{top:6px}.ivu-select-small.ivu-select-multiple .ivu-tag{height:17px;line-height:15px;font-size:12px;padding:0 6px;margin:3px 4px 2px 0}.ivu-select-small.ivu-select-multiple .ivu-tag span{margin-right:14px}.ivu-select-small.ivu-select-multiple .ivu-tag i{top:1px;right:2px}.ivu-select-dropdown-list{min-width:100%;list-style:none}.ivu-select .ivu-select-dropdown{width:auto}.ivu-select-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-select-item:hover{background:#f3f3f3}.ivu-select-item-focus{background:#f3f3f3}.ivu-select-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-select-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-select-item-selected,.ivu-select-item-selected:hover{color:#2d8cf0}.ivu-select-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-select-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-select-large .ivu-select-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-select-item{white-space:normal}}.ivu-select-multiple .ivu-select-item{position:relative}.ivu-select-multiple .ivu-select-item-selected{color:rgba(45,140,240,.9);background:#fff}.ivu-select-multiple .ivu-select-item-focus,.ivu-select-multiple .ivu-select-item-selected:hover{background:#f3f3f3}.ivu-select-multiple .ivu-select-item-selected.ivu-select-multiple .ivu-select-item-focus{color:rgba(40,123,211,.91);background:#fff}.ivu-select-multiple .ivu-select-item-selected:after{display:inline-block;font-family:Ionicons;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;vertical-align:middle;font-size:24px;content:'\F171';color:rgba(45,140,240,.9);position:absolute;top:2px;right:8px}.ivu-select-group{list-style:none;margin:0;padding:0}.ivu-select-group-title{padding-left:8px;font-size:12px;color:#999;height:30px;line-height:30px}.ivu-form-item-error .ivu-select-selection{border:1px solid #ed4014}.ivu-form-item-error .ivu-select-arrow{color:#ed4014}.ivu-form-item-error .ivu-select-visible .ivu-select-selection{border-color:#ed4014;outline:0;-webkit-box-shadow:0 0 0 2px rgba(237,64,20,.2);box-shadow:0 0 0 2px rgba(237,64,20,.2)}.ivu-select-dropdown{width:inherit;max-height:200px;overflow:auto;margin:5px 0;padding:5px 0;background-color:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.2);position:absolute;z-index:900}.ivu-select-dropdown-transfer{z-index:1060;width:auto}.ivu-select-dropdown.ivu-transfer-no-max-height{max-height:none}.ivu-modal .ivu-select-dropdown{position:absolute!important}.ivu-split-wrapper{position:relative;width:100%;height:100%}.ivu-split-pane{position:absolute}.ivu-split-pane.left-pane,.ivu-split-pane.right-pane{top:0;bottom:0}.ivu-split-pane.left-pane{left:0}.ivu-split-pane.right-pane{right:0}.ivu-split-pane.bottom-pane,.ivu-split-pane.top-pane{left:0;right:0}.ivu-split-pane.top-pane{top:0}.ivu-split-pane.bottom-pane{bottom:0}.ivu-split-pane-moving{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-split-trigger{border:1px solid #dcdee2}.ivu-split-trigger-con{position:absolute;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);z-index:10}.ivu-split-trigger-bar-con{position:absolute;overflow:hidden}.ivu-split-trigger-bar-con.vertical{left:1px;top:50%;height:32px;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%)}.ivu-split-trigger-bar-con.horizontal{left:50%;top:1px;width:32px;-webkit-transform:translate(-50%,0);-ms-transform:translate(-50%,0);transform:translate(-50%,0)}.ivu-split-trigger-vertical{width:6px;height:100%;background:#f8f8f9;border-top:none;border-bottom:none;cursor:col-resize}.ivu-split-trigger-vertical .ivu-split-trigger-bar{width:4px;height:1px;background:rgba(23,35,61,.25);float:left;margin-top:3px}.ivu-split-trigger-horizontal{height:6px;width:100%;background:#f8f8f9;border-left:none;border-right:none;cursor:row-resize}.ivu-split-trigger-horizontal .ivu-split-trigger-bar{height:4px;width:1px;background:rgba(23,35,61,.25);float:left;margin-right:3px}.ivu-split-horizontal .ivu-split-trigger-con{top:50%;height:100%;width:0}.ivu-split-vertical .ivu-split-trigger-con{left:50%;height:0;width:100%}.ivu-split .no-select{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-tooltip{display:inline-block}.ivu-tooltip-rel{display:inline-block;position:relative;width:inherit}.ivu-tooltip-popper{display:block;visibility:visible;font-size:12px;line-height:1.5;position:absolute;z-index:1060}.ivu-tooltip-popper[x-placement^=top]{padding:5px 0 8px 0}.ivu-tooltip-popper[x-placement^=right]{padding:0 5px 0 8px}.ivu-tooltip-popper[x-placement^=bottom]{padding:8px 0 5px 0}.ivu-tooltip-popper[x-placement^=left]{padding:0 8px 0 5px}.ivu-tooltip-popper[x-placement^=top] .ivu-tooltip-arrow{bottom:3px;border-width:5px 5px 0;border-top-color:rgba(70,76,91,.9)}.ivu-tooltip-popper[x-placement=top] .ivu-tooltip-arrow{left:50%;margin-left:-5px}.ivu-tooltip-popper[x-placement=top-start] .ivu-tooltip-arrow{left:16px}.ivu-tooltip-popper[x-placement=top-end] .ivu-tooltip-arrow{right:16px}.ivu-tooltip-popper[x-placement^=right] .ivu-tooltip-arrow{left:3px;border-width:5px 5px 5px 0;border-right-color:rgba(70,76,91,.9)}.ivu-tooltip-popper[x-placement=right] .ivu-tooltip-arrow{top:50%;margin-top:-5px}.ivu-tooltip-popper[x-placement=right-start] .ivu-tooltip-arrow{top:8px}.ivu-tooltip-popper[x-placement=right-end] .ivu-tooltip-arrow{bottom:8px}.ivu-tooltip-popper[x-placement^=left] .ivu-tooltip-arrow{right:3px;border-width:5px 0 5px 5px;border-left-color:rgba(70,76,91,.9)}.ivu-tooltip-popper[x-placement=left] .ivu-tooltip-arrow{top:50%;margin-top:-5px}.ivu-tooltip-popper[x-placement=left-start] .ivu-tooltip-arrow{top:8px}.ivu-tooltip-popper[x-placement=left-end] .ivu-tooltip-arrow{bottom:8px}.ivu-tooltip-popper[x-placement^=bottom] .ivu-tooltip-arrow{top:3px;border-width:0 5px 5px;border-bottom-color:rgba(70,76,91,.9)}.ivu-tooltip-popper[x-placement=bottom] .ivu-tooltip-arrow{left:50%;margin-left:-5px}.ivu-tooltip-popper[x-placement=bottom-start] .ivu-tooltip-arrow{left:16px}.ivu-tooltip-popper[x-placement=bottom-end] .ivu-tooltip-arrow{right:16px}.ivu-tooltip-light.ivu-tooltip-popper{display:block;visibility:visible;font-size:12px;line-height:1.5;position:absolute;z-index:1060}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=top]{padding:7px 0 10px 0}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=right]{padding:0 7px 0 10px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=bottom]{padding:10px 0 7px 0}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=left]{padding:0 10px 0 7px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=top] .ivu-tooltip-arrow{bottom:3px;border-width:7px 7px 0;border-top-color:rgba(217,217,217,.5)}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=top] .ivu-tooltip-arrow{left:50%;margin-left:-7px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=top-start] .ivu-tooltip-arrow{left:16px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=top-end] .ivu-tooltip-arrow{right:16px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=right] .ivu-tooltip-arrow{left:3px;border-width:7px 7px 7px 0;border-right-color:rgba(217,217,217,.5)}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=right] .ivu-tooltip-arrow{top:50%;margin-top:-7px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=right-start] .ivu-tooltip-arrow{top:8px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=right-end] .ivu-tooltip-arrow{bottom:8px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=left] .ivu-tooltip-arrow{right:3px;border-width:7px 0 7px 7px;border-left-color:rgba(217,217,217,.5)}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=left] .ivu-tooltip-arrow{top:50%;margin-top:-7px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=left-start] .ivu-tooltip-arrow{top:8px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=left-end] .ivu-tooltip-arrow{bottom:8px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=bottom] .ivu-tooltip-arrow{top:3px;border-width:0 7px 7px;border-bottom-color:rgba(217,217,217,.5)}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=bottom] .ivu-tooltip-arrow{left:50%;margin-left:-7px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=bottom-start] .ivu-tooltip-arrow{left:16px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement=bottom-end] .ivu-tooltip-arrow{right:16px}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=top] .ivu-tooltip-arrow:after{content:" ";bottom:1px;margin-left:-7px;border-bottom-width:0;border-top-width:7px;border-top-color:#fff}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=right] .ivu-tooltip-arrow:after{content:" ";left:1px;bottom:-7px;border-left-width:0;border-right-width:7px;border-right-color:#fff}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=bottom] .ivu-tooltip-arrow:after{content:" ";top:1px;margin-left:-7px;border-top-width:0;border-bottom-width:7px;border-bottom-color:#fff}.ivu-tooltip-light.ivu-tooltip-popper[x-placement^=left] .ivu-tooltip-arrow:after{content:" ";right:1px;border-right-width:0;border-left-width:7px;border-left-color:#fff;bottom:-7px}.ivu-tooltip-inner{max-width:250px;min-height:34px;padding:8px 12px;color:#fff;text-align:left;text-decoration:none;background-color:rgba(70,76,91,.9);border-radius:4px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.2);white-space:nowrap}.ivu-tooltip-inner-with-width{white-space:pre-wrap;text-align:justify}.ivu-tooltip-light .ivu-tooltip-inner{background-color:#fff;color:#515a6e}.ivu-tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.ivu-tooltip-light .ivu-tooltip-arrow{border-width:8px}.ivu-tooltip-light .ivu-tooltip-arrow:after{display:block;width:0;height:0;position:absolute;border-color:transparent;border-style:solid;content:"";border-width:7px}.ivu-poptip{display:inline-block}.ivu-poptip-rel{display:inline-block;position:relative}.ivu-poptip-title{margin:0;padding:8px 16px;position:relative}.ivu-poptip-title:after{content:'';display:block;height:1px;position:absolute;left:8px;right:8px;bottom:0;background-color:#e8eaec}.ivu-poptip-title-inner{color:#17233d;font-size:14px}.ivu-poptip-body{padding:8px 16px}.ivu-poptip-body-content{overflow:auto}.ivu-poptip-body-content-word-wrap{white-space:pre-wrap;text-align:justify}.ivu-poptip-body-content-inner{color:#515a6e}.ivu-poptip-inner{width:100%;background-color:#fff;background-clip:padding-box;border-radius:4px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.2);box-shadow:0 1px 6px rgba(0,0,0,.2);white-space:nowrap}.ivu-poptip-popper{min-width:150px;display:block;visibility:visible;font-size:12px;line-height:1.5;position:absolute;z-index:1060}.ivu-poptip-popper[x-placement^=top]{padding:7px 0 10px 0}.ivu-poptip-popper[x-placement^=right]{padding:0 7px 0 10px}.ivu-poptip-popper[x-placement^=bottom]{padding:10px 0 7px 0}.ivu-poptip-popper[x-placement^=left]{padding:0 10px 0 7px}.ivu-poptip-popper[x-placement^=top] .ivu-poptip-arrow{bottom:3px;border-width:7px 7px 0;border-top-color:rgba(217,217,217,.5)}.ivu-poptip-popper[x-placement=top] .ivu-poptip-arrow{left:50%;margin-left:-7px}.ivu-poptip-popper[x-placement=top-start] .ivu-poptip-arrow{left:16px}.ivu-poptip-popper[x-placement=top-end] .ivu-poptip-arrow{right:16px}.ivu-poptip-popper[x-placement^=right] .ivu-poptip-arrow{left:3px;border-width:7px 7px 7px 0;border-right-color:rgba(217,217,217,.5)}.ivu-poptip-popper[x-placement=right] .ivu-poptip-arrow{top:50%;margin-top:-7px}.ivu-poptip-popper[x-placement=right-start] .ivu-poptip-arrow{top:8px}.ivu-poptip-popper[x-placement=right-end] .ivu-poptip-arrow{bottom:8px}.ivu-poptip-popper[x-placement^=left] .ivu-poptip-arrow{right:3px;border-width:7px 0 7px 7px;border-left-color:rgba(217,217,217,.5)}.ivu-poptip-popper[x-placement=left] .ivu-poptip-arrow{top:50%;margin-top:-7px}.ivu-poptip-popper[x-placement=left-start] .ivu-poptip-arrow{top:8px}.ivu-poptip-popper[x-placement=left-end] .ivu-poptip-arrow{bottom:8px}.ivu-poptip-popper[x-placement^=bottom] .ivu-poptip-arrow{top:3px;border-width:0 7px 7px;border-bottom-color:rgba(217,217,217,.5)}.ivu-poptip-popper[x-placement=bottom] .ivu-poptip-arrow{left:50%;margin-left:-7px}.ivu-poptip-popper[x-placement=bottom-start] .ivu-poptip-arrow{left:16px}.ivu-poptip-popper[x-placement=bottom-end] .ivu-poptip-arrow{right:16px}.ivu-poptip-popper[x-placement^=top] .ivu-poptip-arrow:after{content:" ";bottom:1px;margin-left:-7px;border-bottom-width:0;border-top-width:7px;border-top-color:#fff}.ivu-poptip-popper[x-placement^=right] .ivu-poptip-arrow:after{content:" ";left:1px;bottom:-7px;border-left-width:0;border-right-width:7px;border-right-color:#fff}.ivu-poptip-popper[x-placement^=bottom] .ivu-poptip-arrow:after{content:" ";top:1px;margin-left:-7px;border-top-width:0;border-bottom-width:7px;border-bottom-color:#fff}.ivu-poptip-popper[x-placement^=left] .ivu-poptip-arrow:after{content:" ";right:1px;border-right-width:0;border-left-width:7px;border-left-color:#fff;bottom:-7px}.ivu-poptip-arrow,.ivu-poptip-arrow:after{display:block;width:0;height:0;position:absolute;border-color:transparent;border-style:solid}.ivu-poptip-arrow{border-width:8px}.ivu-poptip-arrow:after{content:"";border-width:7px}.ivu-poptip-confirm .ivu-poptip-popper{max-width:300px}.ivu-poptip-confirm .ivu-poptip-inner{white-space:normal}.ivu-poptip-confirm .ivu-poptip-body{padding:16px 16px 8px}.ivu-poptip-confirm .ivu-poptip-body .ivu-icon{font-size:16px;color:#f90;line-height:18px;position:absolute}.ivu-poptip-confirm .ivu-poptip-body-message{padding-left:20px}.ivu-poptip-confirm .ivu-poptip-footer{text-align:right;padding:8px 16px 16px}.ivu-poptip-confirm .ivu-poptip-footer button{margin-left:4px}.ivu-input{display:inline-block;width:100%;height:32px;line-height:1.5;padding:4px 7px;font-size:12px;border:1px solid #dcdee2;border-radius:4px;color:#515a6e;background-color:#fff;background-image:none;position:relative;cursor:text;-webkit-transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-input::-moz-placeholder{color:#c5c8ce;opacity:1}.ivu-input:-ms-input-placeholder{color:#c5c8ce}.ivu-input::-webkit-input-placeholder{color:#c5c8ce}.ivu-input:hover{border-color:#57a3f3}.ivu-input:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-input[disabled],fieldset[disabled] .ivu-input{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-input[disabled]:hover,fieldset[disabled] .ivu-input:hover{border-color:#e3e5e8}textarea.ivu-input{max-width:100%;height:auto;min-height:32px;vertical-align:bottom;font-size:14px}.ivu-input-large{font-size:14px;padding:6px 7px;height:36px}.ivu-input-small{padding:1px 7px;height:24px;border-radius:3px}.ivu-input-wrapper{display:inline-block;width:100%;position:relative;vertical-align:middle;line-height:normal}.ivu-input-icon{width:32px;height:32px;line-height:32px;font-size:16px;text-align:center;color:#808695;position:absolute;right:0;z-index:3}.ivu-input-hide-icon .ivu-input-icon{display:none}.ivu-input-icon-validate{display:none}.ivu-input-icon-clear{display:none}.ivu-input-wrapper:hover .ivu-input-icon-clear{display:inline-block}.ivu-input-icon-normal+.ivu-input{padding-right:32px}.ivu-input-hide-icon .ivu-input-icon-normal+.ivu-input{padding-right:7px}.ivu-input-wrapper-large .ivu-input-icon{font-size:18px;height:36px;line-height:36px}.ivu-input-wrapper-small .ivu-input-icon{width:24px;font-size:14px;height:24px;line-height:24px}.ivu-input-prefix,.ivu-input-suffix{width:32px;height:100%;text-align:center;position:absolute;left:0;top:0;z-index:1}.ivu-input-prefix i,.ivu-input-suffix i{font-size:16px;line-height:32px;color:#808695}.ivu-input-suffix{left:auto;right:0}.ivu-input-wrapper-small .ivu-input-prefix i,.ivu-input-wrapper-small .ivu-input-suffix i{font-size:14px;line-height:24px}.ivu-input-wrapper-large .ivu-input-prefix i,.ivu-input-wrapper-large .ivu-input-suffix i{font-size:18px;line-height:36px}.ivu-input-with-prefix{padding-left:32px}.ivu-input-with-suffix{padding-right:32px}.ivu-input-search{cursor:pointer;padding:0 16px!important;background:#2d8cf0!important;color:#fff!important;border-color:#2d8cf0!important;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;position:relative;z-index:2}.ivu-input-search i{font-size:16px}.ivu-input-search:hover{background:#57a3f3!important;border-color:#57a3f3!important}.ivu-input-search:active{background:#2b85e4!important;border-color:#2b85e4!important}.ivu-input-search-icon{cursor:pointer;-webkit-transition:color .2s ease-in-out;transition:color .2s ease-in-out}.ivu-input-search-icon:hover{color:inherit}.ivu-input-search:before{content:'';display:block;width:1px;position:absolute;top:-1px;bottom:-1px;left:-1px;background:inherit}.ivu-input-wrapper-small .ivu-input-search{padding:0 12px!important}.ivu-input-wrapper-small .ivu-input-search i{font-size:14px}.ivu-input-wrapper-large .ivu-input-search{padding:0 20px!important}.ivu-input-wrapper-large .ivu-input-search i{font-size:18px}.ivu-input-with-search:hover .ivu-input{border-color:#57a3f3}.ivu-input-group{display:table;width:100%;border-collapse:separate;position:relative;font-size:12px;top:1px}.ivu-input-group-large{font-size:14px}.ivu-input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.ivu-input-group>[class*=col-]{padding-right:8px}.ivu-input-group-append,.ivu-input-group-prepend,.ivu-input-group>.ivu-input{display:table-cell}.ivu-input-group-with-prepend .ivu-input,.ivu-input-group-with-prepend.ivu-input-group-small .ivu-input{border-top-left-radius:0;border-bottom-left-radius:0}.ivu-input-group-with-append .ivu-input,.ivu-input-group-with-append.ivu-input-group-small .ivu-input{border-top-right-radius:0;border-bottom-right-radius:0}.ivu-input-group-append .ivu-btn,.ivu-input-group-prepend .ivu-btn{border-color:transparent;background-color:transparent;color:inherit;margin:-6px -7px}.ivu-input-group-append,.ivu-input-group-prepend{width:1px;white-space:nowrap;vertical-align:middle}.ivu-input-group .ivu-input{width:100%;float:left;margin-bottom:0;position:relative;z-index:2}.ivu-input-group-append,.ivu-input-group-prepend{padding:4px 7px;font-size:inherit;font-weight:400;line-height:1;color:#515a6e;text-align:center;background-color:#f8f8f9;border:1px solid #dcdee2;border-radius:4px}.ivu-input-group-append .ivu-select,.ivu-input-group-prepend .ivu-select{margin:-5px -7px}.ivu-input-group-append .ivu-select-selection,.ivu-input-group-prepend .ivu-select-selection{background-color:inherit;margin:-1px;border:1px solid transparent}.ivu-input-group-append .ivu-select-visible .ivu-select-selection,.ivu-input-group-prepend .ivu-select-visible .ivu-select-selection{-webkit-box-shadow:none;box-shadow:none}.ivu-input-group-prepend,.ivu-input-group>.ivu-input:first-child,.ivu-input-group>span>.ivu-input:first-child{border-bottom-right-radius:0!important;border-top-right-radius:0!important}.ivu-input-group-prepend .ivu--select .ivu--select-selection,.ivu-input-group>.ivu-input:first-child .ivu--select .ivu--select-selection,.ivu-input-group>span>.ivu-input:first-child .ivu--select .ivu--select-selection{border-bottom-right-radius:0;border-top-right-radius:0}.ivu-input-group-prepend{border-right:0}.ivu-input-group-append{border-left:0}.ivu-input-group-append,.ivu-input-group>.ivu-input:last-child{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.ivu-input-group-append .ivu--select .ivu--select-selection,.ivu-input-group>.ivu-input:last-child .ivu--select .ivu--select-selection{border-bottom-left-radius:0;border-top-left-radius:0}.ivu-input-group-large .ivu-input,.ivu-input-group-large>.ivu-input-group-append,.ivu-input-group-large>.ivu-input-group-prepend{font-size:14px;padding:6px 7px;height:36px}.ivu-input-group-small .ivu-input,.ivu-input-group-small>.ivu-input-group-append,.ivu-input-group-small>.ivu-input-group-prepend{padding:1px 7px;height:24px;border-radius:3px}.ivu-form-item-error .ivu-input{border:1px solid #ed4014}.ivu-form-item-error .ivu-input:hover{border-color:#ed4014}.ivu-form-item-error .ivu-input:focus{border-color:#ed4014;outline:0;-webkit-box-shadow:0 0 0 2px rgba(237,64,20,.2);box-shadow:0 0 0 2px rgba(237,64,20,.2)}.ivu-form-item-error .ivu-input-icon{color:#ed4014}.ivu-form-item-error .ivu-input-group-append,.ivu-form-item-error .ivu-input-group-prepend{background-color:#fff;border:1px solid #ed4014}.ivu-form-item-error .ivu-input-group-append .ivu-select-selection,.ivu-form-item-error .ivu-input-group-prepend .ivu-select-selection{background-color:inherit;border:1px solid transparent}.ivu-form-item-error .ivu-input-group-prepend{border-right:0}.ivu-form-item-error .ivu-input-group-append{border-left:0}.ivu-form-item-error .ivu-transfer .ivu-input{display:inline-block;width:100%;height:32px;line-height:1.5;padding:4px 7px;font-size:12px;border:1px solid #dcdee2;border-radius:4px;color:#515a6e;background-color:#fff;background-image:none;position:relative;cursor:text;-webkit-transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,background .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-form-item-error .ivu-transfer .ivu-input::-moz-placeholder{color:#c5c8ce;opacity:1}.ivu-form-item-error .ivu-transfer .ivu-input:-ms-input-placeholder{color:#c5c8ce}.ivu-form-item-error .ivu-transfer .ivu-input::-webkit-input-placeholder{color:#c5c8ce}.ivu-form-item-error .ivu-transfer .ivu-input:hover{border-color:#57a3f3}.ivu-form-item-error .ivu-transfer .ivu-input:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-form-item-error .ivu-transfer .ivu-input[disabled],fieldset[disabled] .ivu-form-item-error .ivu-transfer .ivu-input{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-form-item-error .ivu-transfer .ivu-input[disabled]:hover,fieldset[disabled] .ivu-form-item-error .ivu-transfer .ivu-input:hover{border-color:#e3e5e8}textarea.ivu-form-item-error .ivu-transfer .ivu-input{max-width:100%;height:auto;min-height:32px;vertical-align:bottom;font-size:14px}.ivu-form-item-error .ivu-transfer .ivu-input-large{font-size:14px;padding:6px 7px;height:36px}.ivu-form-item-error .ivu-transfer .ivu-input-small{padding:1px 7px;height:24px;border-radius:3px}.ivu-form-item-error .ivu-transfer .ivu-input-icon{color:#808695}.ivu-form-item-validating .ivu-input-icon-validate{display:inline-block}.ivu-form-item-validating .ivu-input-icon+.ivu-input{padding-right:32px}.ivu-slider{line-height:normal}.ivu-slider-wrap{width:100%;height:4px;margin:16px 0;background-color:#e8eaec;border-radius:3px;vertical-align:middle;position:relative;cursor:pointer}.ivu-slider-button-wrap{width:18px;height:18px;text-align:center;background-color:transparent;position:absolute;top:-4px;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ivu-slider-button-wrap .ivu-tooltip{display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-slider-button{width:12px;height:12px;border:2px solid #57a3f3;border-radius:50%;background-color:#fff;-webkit-transition:all .2s linear;transition:all .2s linear;outline:0}.ivu-slider-button-dragging,.ivu-slider-button:focus,.ivu-slider-button:hover{border-color:#2d8cf0;-webkit-transform:scale(1.5);-ms-transform:scale(1.5);transform:scale(1.5)}.ivu-slider-button:hover{cursor:-webkit-grab;cursor:grab}.ivu-slider-button-dragging,.ivu-slider-button-dragging:hover{cursor:-webkit-grabbing;cursor:grabbing}.ivu-slider-bar{height:4px;background:#57a3f3;border-radius:3px;position:absolute}.ivu-slider-stop{position:absolute;width:4px;height:4px;border-radius:50%;background-color:#ccc;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ivu-slider-disabled{cursor:not-allowed}.ivu-slider-disabled .ivu-slider-wrap{background-color:#ccc;cursor:not-allowed}.ivu-slider-disabled .ivu-slider-bar{background-color:#ccc}.ivu-slider-disabled .ivu-slider-button{border-color:#ccc}.ivu-slider-disabled .ivu-slider-button-dragging,.ivu-slider-disabled .ivu-slider-button:hover{border-color:#ccc}.ivu-slider-disabled .ivu-slider-button:hover{cursor:not-allowed}.ivu-slider-disabled .ivu-slider-button-dragging,.ivu-slider-disabled .ivu-slider-button-dragging:hover{cursor:not-allowed}.ivu-slider-input .ivu-slider-wrap{width:auto;margin-right:100px}.ivu-slider-input .ivu-input-number{float:right;margin-top:-14px}.selectDropDown{width:auto;padding:0;white-space:nowrap;overflow:visible}.ivu-cascader{line-height:normal}.ivu-cascader-rel{display:inline-block;width:100%;position:relative}.ivu-cascader .ivu-input{padding-right:24px;display:block;cursor:pointer}.ivu-cascader-disabled .ivu-input{cursor:not-allowed}.ivu-cascader-label{width:100%;height:100%;line-height:32px;padding:0 7px;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;cursor:pointer;font-size:12px;position:absolute;left:0;top:0}.ivu-cascader-size-large .ivu-cascader-label{line-height:36px;font-size:14px}.ivu-cascader-size-small .ivu-cascader-label{line-height:26px}.ivu-cascader .ivu-cascader-arrow:nth-of-type(1){display:none;cursor:pointer}.ivu-cascader:hover .ivu-cascader-arrow:nth-of-type(1){display:inline-block}.ivu-cascader-show-clear:hover .ivu-cascader-arrow:nth-of-type(2){display:none}.ivu-cascader-arrow{position:absolute;top:50%;right:8px;line-height:1;margin-top:-7px;font-size:14px;color:#808695;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-cascader-visible .ivu-cascader-arrow:nth-of-type(2){-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.ivu-cascader .ivu-select-dropdown{width:auto;padding:0;white-space:nowrap;overflow:visible}.ivu-cascader .ivu-cascader-menu-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-cascader .ivu-cascader-menu-item:hover{background:#f3f3f3}.ivu-cascader .ivu-cascader-menu-item-focus{background:#f3f3f3}.ivu-cascader .ivu-cascader-menu-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-cascader .ivu-cascader-menu-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-cascader .ivu-cascader-menu-item-selected,.ivu-cascader .ivu-cascader-menu-item-selected:hover{color:#2d8cf0}.ivu-cascader .ivu-cascader-menu-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-cascader .ivu-cascader-menu-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-cascader .ivu-cascader-large .ivu-cascader-menu-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-cascader .ivu-cascader-menu-item{white-space:normal}}.ivu-cascader .ivu-select-item span{color:#ed4014}.ivu-cascader-dropdown{padding:5px 0}.ivu-cascader-dropdown .ivu-select-dropdown-list{max-height:190px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:auto}.ivu-cascader-not-found-tip{padding:5px 0;text-align:center;color:#c5c8ce}.ivu-cascader-not-found-tip li:not([class^=ivu-]){list-style:none;margin-bottom:0}.ivu-cascader-not-found .ivu-select-dropdown{width:inherit}.ivu-cascader-menu{display:inline-block;min-width:100px;height:180px;margin:0;padding:5px 0!important;vertical-align:top;list-style:none;border-right:1px solid #e8eaec;overflow:auto}.ivu-cascader-menu:last-child{border-right-color:transparent;margin-right:-1px}.ivu-cascader-menu .ivu-cascader-menu-item{position:relative;padding-right:24px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-cascader-menu .ivu-cascader-menu-item i{font-size:12px;position:absolute;right:15px;top:50%;margin-top:-6px}.ivu-cascader-menu .ivu-cascader-menu-item-active{background-color:#f3f3f3;color:#2d8cf0}.ivu-cascader-transfer{z-index:1060;width:auto;padding:0;white-space:nowrap;overflow:visible}.ivu-cascader-transfer .ivu-cascader-menu-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-cascader-transfer .ivu-cascader-menu-item:hover{background:#f3f3f3}.ivu-cascader-transfer .ivu-cascader-menu-item-focus{background:#f3f3f3}.ivu-cascader-transfer .ivu-cascader-menu-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-cascader-transfer .ivu-cascader-menu-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-cascader-transfer .ivu-cascader-menu-item-selected,.ivu-cascader-transfer .ivu-cascader-menu-item-selected:hover{color:#2d8cf0}.ivu-cascader-transfer .ivu-cascader-menu-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-cascader-transfer .ivu-cascader-menu-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-cascader-transfer .ivu-cascader-large .ivu-cascader-menu-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-cascader-transfer .ivu-cascader-menu-item{white-space:normal}}.ivu-cascader-transfer .ivu-select-item span{color:#ed4014}.ivu-cascader-transfer .ivu-cascader-menu-item{padding-right:24px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-cascader-transfer .ivu-cascader-menu-item-active{background-color:#f3f3f3;color:#2d8cf0}.ivu-form-item-error .ivu-cascader-arrow{color:#ed4014}.ivu-transfer{position:relative;line-height:1.5}.ivu-transfer-list{display:inline-block;width:180px;height:210px;font-size:12px;vertical-align:middle;position:relative;padding-top:35px}.ivu-transfer-list-with-footer{padding-bottom:35px}.ivu-transfer-list-header{padding:8px 16px;background:#f9fafc;color:#515a6e;border:1px solid #dcdee2;border-bottom:1px solid #e8eaec;border-radius:6px 6px 0 0;overflow:hidden;position:absolute;top:0;left:0;width:100%}.ivu-transfer-list-header-title{cursor:pointer}.ivu-transfer-list-header>span{padding-left:4px}.ivu-transfer-list-header-count{margin:0!important;float:right}.ivu-transfer-list-body{height:100%;border:1px solid #dcdee2;border-top:none;border-radius:0 0 6px 6px;position:relative;overflow:hidden}.ivu-transfer-list-body-with-search{padding-top:34px}.ivu-transfer-list-body-with-footer{border-radius:0}.ivu-transfer-list-content{height:100%;padding:4px 0;overflow:auto}.ivu-transfer-list-content-item{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ivu-transfer-list-content-item>span{padding-left:4px}.ivu-transfer-list-content-not-found{display:none;text-align:center;color:#c5c8ce}li.ivu-transfer-list-content-not-found:only-child{display:block}.ivu-transfer-list-body-with-search .ivu-transfer-list-content{padding:6px 0 0}.ivu-transfer-list-body-search-wrapper{padding:8px 8px 0;position:absolute;top:0;left:0;right:0}.ivu-transfer-list-search{position:relative}.ivu-transfer-list-footer{border:1px solid #dcdee2;border-top:none;border-radius:0 0 6px 6px;position:absolute;bottom:0;left:0;right:0;zoom:1}.ivu-transfer-list-footer:after,.ivu-transfer-list-footer:before{content:"";display:table}.ivu-transfer-list-footer:after{clear:both;visibility:hidden;font-size:0;height:0}.ivu-transfer-operation{display:inline-block;margin:0 16px;vertical-align:middle}.ivu-transfer-operation .ivu-btn{display:block;min-width:24px}.ivu-transfer-operation .ivu-btn:first-child{margin-bottom:12px}.ivu-transfer-operation .ivu-btn span i,.ivu-transfer-operation .ivu-btn span span{vertical-align:middle}.ivu-transfer-list-content-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-transfer-list-content-item:hover{background:#f3f3f3}.ivu-transfer-list-content-item-focus{background:#f3f3f3}.ivu-transfer-list-content-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-transfer-list-content-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-transfer-list-content-item-selected,.ivu-transfer-list-content-item-selected:hover{color:#2d8cf0}.ivu-transfer-list-content-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-transfer-list-content-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-transfer-large .ivu-transfer-list-content-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-transfer-list-content-item{white-space:normal}}.ivu-table{width:inherit;height:100%;max-width:100%;overflow:hidden;color:#515a6e;font-size:12px;background-color:#fff;-webkit-box-sizing:border-box;box-sizing:border-box}.ivu-table-wrapper{position:relative;border:1px solid #dcdee2;border-bottom:0;border-right:0}.ivu-table-hide{opacity:0}.ivu-table:before{content:'';width:100%;height:1px;position:absolute;left:0;bottom:0;background-color:#dcdee2;z-index:1}.ivu-table:after{content:'';width:1px;height:100%;position:absolute;top:0;right:0;background-color:#dcdee2;z-index:3}.ivu-table-footer,.ivu-table-title{height:48px;line-height:48px;border-bottom:1px solid #e8eaec}.ivu-table-footer{border-bottom:none}.ivu-table-header{overflow:hidden}.ivu-table-overflowX{overflow-x:scroll}.ivu-table-overflowY{overflow-y:scroll}.ivu-table-tip{overflow-x:auto;overflow-y:hidden}.ivu-table-with-fixed-top.ivu-table-with-footer .ivu-table-footer{border-top:1px solid #dcdee2}.ivu-table-with-fixed-top.ivu-table-with-footer tbody tr:last-child td{border-bottom:none}.ivu-table td,.ivu-table th{min-width:0;height:48px;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:left;text-overflow:ellipsis;vertical-align:middle;border-bottom:1px solid #e8eaec}.ivu-table th{height:40px;white-space:nowrap;overflow:hidden;background-color:#f8f8f9}.ivu-table td{background-color:#fff;-webkit-transition:background-color .2s ease-in-out;transition:background-color .2s ease-in-out}td.ivu-table-column-left,th.ivu-table-column-left{text-align:left}td.ivu-table-column-center,th.ivu-table-column-center{text-align:center}td.ivu-table-column-right,th.ivu-table-column-right{text-align:right}.ivu-table table{table-layout:fixed}.ivu-table-border td,.ivu-table-border th{border-right:1px solid #e8eaec}.ivu-table-cell{padding-left:18px;padding-right:18px;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;-webkit-box-sizing:border-box;box-sizing:border-box}.ivu-table-cell-ellipsis{word-break:keep-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ivu-table-cell-tooltip{width:100%}.ivu-table-cell-tooltip-content{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-table-cell-with-expand{height:47px;line-height:47px;padding:0;text-align:center}.ivu-table-cell-expand{cursor:pointer;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out}.ivu-table-cell-expand i{font-size:14px}.ivu-table-cell-expand-expanded{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.ivu-table-cell-sort{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-table-cell-with-selection .ivu-checkbox-wrapper{margin-right:0}.ivu-table-hidden{visibility:hidden}th .ivu-table-cell{display:inline-block;word-wrap:normal;vertical-align:middle}td.ivu-table-expanded-cell{padding:20px 50px;background:#f8f8f9}.ivu-table-stripe .ivu-table-body tr:nth-child(2n) td,.ivu-table-stripe .ivu-table-fixed-body tr:nth-child(2n) td{background-color:#f8f8f9}.ivu-table-stripe .ivu-table-body tr.ivu-table-row-hover td,.ivu-table-stripe .ivu-table-fixed-body tr.ivu-table-row-hover td{background-color:#ebf7ff}tr.ivu-table-row-hover td{background-color:#ebf7ff}.ivu-table-large{font-size:14px}.ivu-table-large th{height:48px}.ivu-table-large td{height:60px}.ivu-table-large-footer,.ivu-table-large-title{height:60px;line-height:60px}.ivu-table-large .ivu-table-cell-with-expand{height:59px;line-height:59px}.ivu-table-large .ivu-table-cell-with-expand i{font-size:16px}.ivu-table-small th{height:32px}.ivu-table-small td{height:40px}.ivu-table-small-footer,.ivu-table-small-title{height:40px;line-height:40px}.ivu-table-small .ivu-table-cell-with-expand{height:39px;line-height:39px}.ivu-table-row-highlight td,.ivu-table-stripe .ivu-table-body tr.ivu-table-row-highlight:nth-child(2n) td,.ivu-table-stripe .ivu-table-fixed-body tr.ivu-table-row-highlight:nth-child(2n) td,tr.ivu-table-row-highlight.ivu-table-row-hover td{background-color:#ebf7ff}.ivu-table-fixed,.ivu-table-fixed-right{position:absolute;top:0;left:0;-webkit-box-shadow:2px 0 6px -2px rgba(0,0,0,.2);box-shadow:2px 0 6px -2px rgba(0,0,0,.2)}.ivu-table-fixed-right::before,.ivu-table-fixed::before{content:'';width:100%;height:1px;background-color:#dcdee2;position:absolute;left:0;bottom:0;z-index:4}.ivu-table-fixed-right{top:0;left:auto;right:0;-webkit-box-shadow:-2px 0 6px -2px rgba(0,0,0,.2);box-shadow:-2px 0 6px -2px rgba(0,0,0,.2)}.ivu-table-fixed-right-header{position:absolute;top:-1px;right:0;background-color:#f8f8f9;border-top:1px solid #dcdee2;border-bottom:1px solid #e8eaec}.ivu-table-fixed-header{overflow:hidden}.ivu-table-fixed-body{overflow:hidden;position:relative;z-index:3}.ivu-table-fixed-shadow{width:1px;height:100%;position:absolute;top:0;right:0;-webkit-box-shadow:1px 0 6px rgba(0,0,0,.2);box-shadow:1px 0 6px rgba(0,0,0,.2);overflow:hidden;z-index:1}.ivu-table-sort{display:inline-block;width:14px;height:12px;margin-top:-1px;vertical-align:middle;overflow:hidden;cursor:pointer;position:relative}.ivu-table-sort i{display:block;height:6px;line-height:6px;overflow:hidden;position:absolute;color:#c5c8ce;-webkit-transition:color .2s ease-in-out;transition:color .2s ease-in-out;font-size:16px}.ivu-table-sort i:hover{color:inherit}.ivu-table-sort i.on{color:#2d8cf0}.ivu-table-sort i:first-child{top:0}.ivu-table-sort i:last-child{bottom:0}.ivu-table-filter{display:inline-block;cursor:pointer;position:relative}.ivu-table-filter i{color:#c5c8ce;-webkit-transition:color .2s ease-in-out;transition:color .2s ease-in-out}.ivu-table-filter i:hover{color:inherit}.ivu-table-filter i.on{color:#2d8cf0}.ivu-table-filter-list{padding:8px 0 0}.ivu-table-filter-list-item{padding:0 12px 8px}.ivu-table-filter-list-item .ivu-checkbox-wrapper+.ivu-checkbox-wrapper{margin:0}.ivu-table-filter-list-item label{display:block}.ivu-table-filter-list-item label>span{margin-right:4px}.ivu-table-filter-list ul{padding-bottom:8px}.ivu-table-filter-list .ivu-table-filter-select-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-table-filter-list .ivu-table-filter-select-item:hover{background:#f3f3f3}.ivu-table-filter-list .ivu-table-filter-select-item-focus{background:#f3f3f3}.ivu-table-filter-list .ivu-table-filter-select-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-table-filter-list .ivu-table-filter-select-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-table-filter-list .ivu-table-filter-select-item-selected,.ivu-table-filter-list .ivu-table-filter-select-item-selected:hover{color:#2d8cf0}.ivu-table-filter-list .ivu-table-filter-select-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-table-filter-list .ivu-table-filter-select-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-table-filter-list .ivu-table-large .ivu-table-filter-select-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-table-filter-list .ivu-table-filter-select-item{white-space:normal}}.ivu-table-filter-footer{padding:4px;border-top:1px solid #e8eaec;overflow:hidden}.ivu-table-filter-footer button:first-child{float:left}.ivu-table-filter-footer button:last-child{float:right}.ivu-table-tip table{width:100%}.ivu-table-tip table td{text-align:center}.ivu-table-expanded-hidden{visibility:hidden}.ivu-table-popper{min-width:0;text-align:left}.ivu-table-popper .ivu-poptip-body{padding:0}.ivu-dropdown{display:inline-block}.ivu-dropdown .ivu-select-dropdown{overflow:visible;max-height:none}.ivu-dropdown .ivu-dropdown{width:100%}.ivu-dropdown-rel{position:relative}.ivu-dropdown-rel-user-select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-dropdown-menu{min-width:100px}.ivu-dropdown-transfer{width:auto}.ivu-dropdown-item-selected,.ivu-dropdown-item.ivu-dropdown-item-selected:hover{background:#f0faff}.ivu-dropdown-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-dropdown-item:hover{background:#f3f3f3}.ivu-dropdown-item-focus{background:#f3f3f3}.ivu-dropdown-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-dropdown-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-dropdown-item-selected,.ivu-dropdown-item-selected:hover{color:#2d8cf0}.ivu-dropdown-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-dropdown-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-dropdown-large .ivu-dropdown-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-dropdown-item{white-space:normal}}.ivu-tabs{-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;overflow:hidden;color:#515a6e;zoom:1}.ivu-tabs:after,.ivu-tabs:before{content:"";display:table}.ivu-tabs:after{clear:both;visibility:hidden;font-size:0;height:0}.ivu-tabs-bar{outline:0}.ivu-tabs-ink-bar{height:2px;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:#2d8cf0;position:absolute;left:0;bottom:1px;z-index:1;-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out;-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.ivu-tabs-bar{border-bottom:1px solid #dcdee2;margin-bottom:16px}.ivu-tabs-nav-container{margin-bottom:-1px;line-height:1.5;font-size:14px;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap;overflow:hidden;position:relative;zoom:1}.ivu-tabs-nav-container:after,.ivu-tabs-nav-container:before{content:"";display:table}.ivu-tabs-nav-container:after{clear:both;visibility:hidden;font-size:0;height:0}.ivu-tabs-nav-container:focus{outline:0}.ivu-tabs-nav-container:focus .ivu-tabs-tab-focused{border-color:#57a3f3!important}.ivu-tabs-nav-container-scrolling{padding-left:32px;padding-right:32px}.ivu-tabs-nav-wrap{overflow:hidden;margin-bottom:-1px}.ivu-tabs-nav-scroll{overflow:hidden;white-space:nowrap}.ivu-tabs-nav-right{float:right;margin-left:5px}.ivu-tabs-nav-prev{position:absolute;line-height:32px;cursor:pointer;left:0}.ivu-tabs-nav-next{position:absolute;line-height:32px;cursor:pointer;right:0}.ivu-tabs-nav-scrollable{padding:0 12px}.ivu-tabs-nav-scroll-disabled{display:none}.ivu-tabs-nav{padding-left:0;margin:0;float:left;list-style:none;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;-webkit-transition:-webkit-transform .5s ease-in-out;transition:-webkit-transform .5s ease-in-out;transition:transform .5s ease-in-out;transition:transform .5s ease-in-out,-webkit-transform .5s ease-in-out}.ivu-tabs-nav:after,.ivu-tabs-nav:before{display:table;content:" "}.ivu-tabs-nav:after{clear:both}.ivu-tabs-nav .ivu-tabs-tab-disabled{pointer-events:none;cursor:default;color:#ccc}.ivu-tabs-nav .ivu-tabs-tab{display:inline-block;height:100%;padding:8px 16px;margin-right:16px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer;text-decoration:none;position:relative;-webkit-transition:color .3s ease-in-out;transition:color .3s ease-in-out}.ivu-tabs-nav .ivu-tabs-tab:hover{color:#57a3f3}.ivu-tabs-nav .ivu-tabs-tab:active{color:#2b85e4}.ivu-tabs-nav .ivu-tabs-tab .ivu-icon{width:14px;height:14px;margin-right:8px}.ivu-tabs-nav .ivu-tabs-tab-active{color:#2d8cf0}.ivu-tabs-mini .ivu-tabs-nav-container{font-size:14px}.ivu-tabs-mini .ivu-tabs-tab{margin-right:0;padding:8px 16px;font-size:12px}.ivu-tabs .ivu-tabs-content-animated{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;will-change:transform;-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}.ivu-tabs .ivu-tabs-tabpane{-ms-flex-negative:0;flex-shrink:0;width:100%;-webkit-transition:opacity .3s;transition:opacity .3s;opacity:1;outline:0}.ivu-tabs .ivu-tabs-tabpane-inactive{opacity:0;height:0}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-nav-container{height:32px}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-ink-bar{visibility:hidden}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab{margin:0;margin-right:4px;height:31px;padding:5px 16px 4px;border:1px solid #dcdee2;border-bottom:0;border-radius:4px 4px 0 0;-webkit-transition:all .3s ease-in-out;transition:all .3s ease-in-out;background:#f8f8f9}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab-active{height:32px;padding-bottom:5px;background:#fff;-webkit-transform:translateZ(0);transform:translateZ(0);border-color:#dcdee2;color:#2d8cf0}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-nav-wrap{margin-bottom:0}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab .ivu-icon-ios-close{width:0;height:22px;font-size:22px;margin-right:0;color:#999;text-align:right;vertical-align:middle;overflow:hidden;position:relative;top:-1px;-webkit-transform-origin:100% 50%;-ms-transform-origin:100% 50%;transform-origin:100% 50%;-webkit-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab .ivu-icon-ios-close:hover{color:#444}.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab-active .ivu-icon-ios-close,.ivu-tabs.ivu-tabs-card>.ivu-tabs-bar .ivu-tabs-tab:hover .ivu-icon-ios-close{width:22px;-webkit-transform:translateZ(0);transform:translateZ(0);margin-right:-6px}.ivu-tabs-no-animation>.ivu-tabs-content{-webkit-transform:none!important;-ms-transform:none!important;transform:none!important}.ivu-tabs-no-animation>.ivu-tabs-content>.ivu-tabs-tabpane-inactive{display:none}.ivu-menu{display:block;margin:0;padding:0;outline:0;list-style:none;color:#515a6e;font-size:14px;position:relative;z-index:900}.ivu-menu-horizontal{height:60px;line-height:60px}.ivu-menu-horizontal.ivu-menu-light:after{content:'';display:block;width:100%;height:1px;background:#dcdee2;position:absolute;bottom:0;left:0}.ivu-menu-vertical.ivu-menu-light:after{content:'';display:block;width:1px;height:100%;background:#dcdee2;position:absolute;top:0;bottom:0;right:0;z-index:1}.ivu-menu-light{background:#fff}.ivu-menu-dark{background:#515a6e}.ivu-menu-primary{background:#2d8cf0}.ivu-menu-item{display:block;outline:0;list-style:none;font-size:14px;position:relative;z-index:1;cursor:pointer;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.ivu-menu-item{color:inherit}a.ivu-menu-item:active,a.ivu-menu-item:hover{color:inherit}.ivu-menu-item>i{margin-right:6px}.ivu-menu-submenu-title span>i,.ivu-menu-submenu-title>i{margin-right:8px}.ivu-menu-horizontal .ivu-menu-item,.ivu-menu-horizontal .ivu-menu-submenu{float:left;padding:0 20px;position:relative;cursor:pointer;z-index:3;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-menu-light.ivu-menu-horizontal .ivu-menu-item,.ivu-menu-light.ivu-menu-horizontal .ivu-menu-submenu{height:inherit;line-height:inherit;border-bottom:2px solid transparent;color:#515a6e}.ivu-menu-light.ivu-menu-horizontal .ivu-menu-item-active,.ivu-menu-light.ivu-menu-horizontal .ivu-menu-item:hover,.ivu-menu-light.ivu-menu-horizontal .ivu-menu-submenu-active,.ivu-menu-light.ivu-menu-horizontal .ivu-menu-submenu:hover{color:#2d8cf0;border-bottom:2px solid #2d8cf0}.ivu-menu-dark.ivu-menu-horizontal .ivu-menu-item,.ivu-menu-dark.ivu-menu-horizontal .ivu-menu-submenu{color:rgba(255,255,255,.7)}.ivu-menu-dark.ivu-menu-horizontal .ivu-menu-item-active,.ivu-menu-dark.ivu-menu-horizontal .ivu-menu-item:hover,.ivu-menu-dark.ivu-menu-horizontal .ivu-menu-submenu-active,.ivu-menu-dark.ivu-menu-horizontal .ivu-menu-submenu:hover{color:#fff}.ivu-menu-primary.ivu-menu-horizontal .ivu-menu-item,.ivu-menu-primary.ivu-menu-horizontal .ivu-menu-submenu{color:#fff}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown{min-width:100%;width:auto;max-height:none}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item{height:auto;line-height:normal;border-bottom:0;float:none}.ivu-menu-item-group{line-height:normal}.ivu-menu-item-group-title{height:30px;line-height:30px;padding-left:8px;font-size:12px;color:#999}.ivu-menu-item-group>ul{padding:0!important;list-style:none!important}.ivu-menu-vertical .ivu-menu-item,.ivu-menu-vertical .ivu-menu-submenu-title{padding:14px 24px;position:relative;cursor:pointer;z-index:1;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-menu-vertical .ivu-menu-item:hover,.ivu-menu-vertical .ivu-menu-submenu-title:hover{color:#2d8cf0}.ivu-menu-vertical .ivu-menu-submenu-title-icon{float:right;position:relative;top:4px}.ivu-menu-submenu-title-icon{-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out}.ivu-menu-opened>*>.ivu-menu-submenu-title-icon{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.ivu-menu-vertical .ivu-menu-submenu-nested{padding-left:20px}.ivu-menu-vertical .ivu-menu-submenu .ivu-menu-item{padding-left:43px}.ivu-menu-vertical .ivu-menu-item-group-title{height:48px;line-height:48px;font-size:14px;padding-left:28px}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-item-group-title{color:rgba(255,255,255,.36)}.ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu){color:#2d8cf0;background:#f0faff;z-index:2}.ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu):after{content:'';display:block;width:2px;position:absolute;top:0;bottom:0;right:0;background:#2d8cf0}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-item,.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu-title{color:rgba(255,255,255,.7)}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu),.ivu-menu-dark.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu):hover,.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu-title-active:not(.ivu-menu-submenu),.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu-title-active:not(.ivu-menu-submenu):hover{background:#363e4f}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-item:hover,.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu-title:hover{color:#fff;background:#515a6e}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu),.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu-title-active:not(.ivu-menu-submenu){color:#2d8cf0}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu .ivu-menu-item:hover{color:#fff;background:0 0!important}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu .ivu-menu-item-active,.ivu-menu-dark.ivu-menu-vertical .ivu-menu-submenu .ivu-menu-item-active:hover{border-right:none;color:#fff;background:#2d8cf0!important}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-child-item-active>.ivu-menu-submenu-title{color:#fff}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-opened{background:#363e4f}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-opened .ivu-menu-submenu-title{background:#515a6e}.ivu-menu-dark.ivu-menu-vertical .ivu-menu-opened .ivu-menu-submenu-has-parent-submenu .ivu-menu-submenu-title{background:0 0}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item:hover{background:#f3f3f3}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-focus{background:#f3f3f3}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-selected,.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-selected:hover{color:#2d8cf0}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-menu-large .ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item{white-space:normal}}.ivu-menu-horizontal .ivu-menu-submenu .ivu-select-dropdown .ivu-menu-item{padding:7px 16px 8px;font-size:14px!important}.ivu-date-picker{display:inline-block;line-height:normal}.ivu-date-picker-rel{position:relative}.ivu-date-picker .ivu-select-dropdown{width:auto;padding:0;overflow:visible;max-height:none}.ivu-date-picker-cells{width:196px;margin:10px;white-space:normal}.ivu-date-picker-cells span{display:inline-block;width:24px;height:24px}.ivu-date-picker-cells span em{display:inline-block;width:24px;height:24px;line-height:24px;margin:2px;font-style:normal;border-radius:3px;text-align:center;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-date-picker-cells-header span{line-height:24px;text-align:center;margin:2px;color:#c5c8ce}.ivu-date-picker-cells-cell:hover em{background:#e1f0fe}.ivu-date-picker-cells-focused em{-webkit-box-shadow:0 0 0 1px #2d8cf0 inset;box-shadow:0 0 0 1px #2d8cf0 inset}span.ivu-date-picker-cells-cell{width:28px;height:28px;cursor:pointer}.ivu-date-picker-cells-cell-next-month em,.ivu-date-picker-cells-cell-prev-month em{color:#c5c8ce}.ivu-date-picker-cells-cell-next-month:hover em,.ivu-date-picker-cells-cell-prev-month:hover em{background:0 0}span.ivu-date-picker-cells-cell-disabled,span.ivu-date-picker-cells-cell-disabled:hover,span.ivu-date-picker-cells-cell-week-label,span.ivu-date-picker-cells-cell-week-label:hover{cursor:not-allowed;color:#c5c8ce}span.ivu-date-picker-cells-cell-disabled em,span.ivu-date-picker-cells-cell-disabled:hover em,span.ivu-date-picker-cells-cell-week-label em,span.ivu-date-picker-cells-cell-week-label:hover em{color:inherit;background:inherit}span.ivu-date-picker-cells-cell-disabled,span.ivu-date-picker-cells-cell-disabled:hover{background:#f7f7f7}.ivu-date-picker-cells-cell-today em{position:relative}.ivu-date-picker-cells-cell-today em:after{content:'';display:block;width:6px;height:6px;border-radius:50%;background:#2d8cf0;position:absolute;top:1px;right:1px}.ivu-date-picker-cells-cell-range{position:relative}.ivu-date-picker-cells-cell-range em{position:relative;z-index:1}.ivu-date-picker-cells-cell-range:before{content:'';display:block;background:#e1f0fe;border-radius:0;border:0;position:absolute;top:2px;bottom:2px;left:0;right:0}.ivu-date-picker-cells-cell-selected em,.ivu-date-picker-cells-cell-selected:hover em{background:#2d8cf0;color:#fff}span.ivu-date-picker-cells-cell-disabled.ivu-date-picker-cells-cell-selected em{background:#c5c8ce;color:#f7f7f7}.ivu-date-picker-cells-cell-today.ivu-date-picker-cells-cell-selected em:after{background:#fff}.ivu-date-picker-cells-show-week-numbers{width:226px}.ivu-date-picker-cells-month,.ivu-date-picker-cells-year{margin-top:14px}.ivu-date-picker-cells-month span,.ivu-date-picker-cells-year span{width:40px;height:28px;line-height:28px;margin:10px 12px;border-radius:3px}.ivu-date-picker-cells-month span em,.ivu-date-picker-cells-year span em{width:40px;height:28px;line-height:28px;margin:0}.ivu-date-picker-cells-month .ivu-date-picker-cells-cell-focused,.ivu-date-picker-cells-year .ivu-date-picker-cells-cell-focused{background-color:#d5e8fc}.ivu-date-picker-header{height:32px;line-height:32px;text-align:center;border-bottom:1px solid #e8eaec}.ivu-date-picker-header-label{cursor:pointer;-webkit-transition:color .2s ease-in-out;transition:color .2s ease-in-out}.ivu-date-picker-header-label:hover{color:#2d8cf0}.ivu-date-picker-btn-pulse{background-color:#d5e8fc!important;border-radius:4px;-webkit-transition:background-color .2s ease-in-out;transition:background-color .2s ease-in-out}.ivu-date-picker-prev-btn{float:left}.ivu-date-picker-prev-btn-arrow-double{margin-left:10px}.ivu-date-picker-prev-btn-arrow-double i:after{content:"\F115";margin-left:-8px}.ivu-date-picker-next-btn{float:right}.ivu-date-picker-next-btn-arrow-double{margin-right:10px}.ivu-date-picker-next-btn-arrow-double i:after{content:"\F11F";margin-left:-8px}.ivu-date-picker-with-range .ivu-picker-panel-body{min-width:432px}.ivu-date-picker-with-range .ivu-picker-panel-content{float:left}.ivu-date-picker-with-range .ivu-picker-cells-show-week-numbers{min-width:492px}.ivu-date-picker-with-week-numbers .ivu-picker-panel-body-date{min-width:492px}.ivu-date-picker-transfer{z-index:1060;max-height:none;width:auto}.ivu-date-picker-focused input{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-picker-panel-icon-btn{display:inline-block;width:20px;height:24px;line-height:26px;margin-top:4px;text-align:center;cursor:pointer;color:#c5c8ce;-webkit-transition:color .2s ease-in-out;transition:color .2s ease-in-out}.ivu-picker-panel-icon-btn:hover{color:#2d8cf0}.ivu-picker-panel-icon-btn i{font-size:14px}.ivu-picker-panel-body-wrapper.ivu-picker-panel-with-sidebar{padding-left:92px}.ivu-picker-panel-sidebar{width:92px;float:left;margin-left:-92px;position:absolute;top:0;bottom:0;background:#f8f8f9;border-right:1px solid #e8eaec;border-radius:4px 0 0 4px;overflow:auto}.ivu-picker-panel-shortcut{padding:6px 15px 6px 15px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-picker-panel-shortcut:hover{background:#e8eaec}.ivu-picker-panel-body{float:left}.ivu-picker-confirm{border-top:1px solid #e8eaec;text-align:right;padding:8px;clear:both}.ivu-picker-confirm>span{color:#2d8cf0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;float:left;padding:2px 0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-picker-confirm>span:hover{color:#57a3f3}.ivu-picker-confirm>span:active{color:#2b85e4}.ivu-picker-confirm-time{float:left}.ivu-time-picker-cells{min-width:112px}.ivu-time-picker-cells-with-seconds{min-width:168px}.ivu-time-picker-cells-list{width:56px;max-height:144px;float:left;overflow:hidden;border-left:1px solid #e8eaec;position:relative}.ivu-time-picker-cells-list:hover{overflow-y:auto}.ivu-time-picker-cells-list:first-child{border-left:none;border-radius:4px 0 0 4px}.ivu-time-picker-cells-list:last-child{border-radius:0 4px 4px 0}.ivu-time-picker-cells-list ul{width:100%;margin:0;padding:0 0 120px 0;list-style:none}.ivu-time-picker-cells-list ul li{width:100%;height:24px;line-height:24px;margin:0;padding:0 0 0 16px;-webkit-box-sizing:content-box;box-sizing:content-box;text-align:left;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;list-style:none;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-time-picker-cells-cell:hover{background:#f3f3f3}.ivu-time-picker-cells-cell-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-time-picker-cells-cell-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-time-picker-cells-cell-selected,.ivu-time-picker-cells-cell-selected:hover{color:#2d8cf0;background:#f3f3f3}.ivu-time-picker-cells-cell-focused{background-color:#d5e8fc}.ivu-time-picker-header{height:32px;line-height:32px;text-align:center;border-bottom:1px solid #e8eaec}.ivu-time-picker-with-range .ivu-picker-panel-body{min-width:228px}.ivu-time-picker-with-range .ivu-picker-panel-content{float:left;position:relative}.ivu-time-picker-with-range .ivu-picker-panel-content:after{content:'';display:block;width:2px;position:absolute;top:31px;bottom:0;right:-2px;background:#e8eaec;z-index:1}.ivu-time-picker-with-range .ivu-picker-panel-content-right{float:right}.ivu-time-picker-with-range .ivu-picker-panel-content-right:after{right:auto;left:-2px}.ivu-time-picker-with-range .ivu-time-picker-cells-list:first-child{border-radius:0}.ivu-time-picker-with-range .ivu-time-picker-cells-list:last-child{border-radius:0}.ivu-time-picker-with-range.ivu-time-picker-with-seconds .ivu-picker-panel-body{min-width:340px}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells{min-width:216px}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-with-seconds{min-width:216px}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-with-seconds .ivu-time-picker-cells-list{width:72px}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-with-seconds .ivu-time-picker-cells-list ul li{padding:0 0 0 28px}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-list{width:108px;max-height:216px}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-list:first-child{border-radius:0}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-list:last-child{border-radius:0}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-list ul{padding:0 0 192px 0}.ivu-picker-panel-content .ivu-picker-panel-content .ivu-time-picker-cells-list ul li{padding:0 0 0 46px}.ivu-form .ivu-form-item-label{text-align:right;vertical-align:middle;float:left;font-size:12px;color:#515a6e;line-height:1;padding:10px 12px 10px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.ivu-form-label-left .ivu-form-item-label{text-align:left}.ivu-form-label-top .ivu-form-item-label{float:none;display:inline-block;padding:0 0 10px 0}.ivu-form-inline .ivu-form-item{display:inline-block;margin-right:10px;vertical-align:top}.ivu-form-item{margin-bottom:24px;vertical-align:top;zoom:1}.ivu-form-item:after,.ivu-form-item:before{content:"";display:table}.ivu-form-item:after{clear:both;visibility:hidden;font-size:0;height:0}.ivu-form-item-content{position:relative;line-height:32px;font-size:12px}.ivu-form-item .ivu-form-item{margin-bottom:0}.ivu-form-item .ivu-form-item .ivu-form-item-content{margin-left:0!important}.ivu-form-item-error-tip{position:absolute;top:100%;left:0;line-height:1;padding-top:6px;color:#ed4014}.ivu-form-item-required .ivu-form-item-label:before{content:'*';display:inline-block;margin-right:4px;line-height:1;font-family:SimSun;font-size:12px;color:#ed4014}.ivu-carousel{position:relative;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:pan-y;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.ivu-carousel-list,.ivu-carousel-track{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ivu-carousel-list{position:relative;display:block;overflow:hidden;margin:0;padding:0}.ivu-carousel-track{position:relative;top:0;left:0;display:block;overflow:hidden;z-index:1}.ivu-carousel-track.higher{z-index:2}.ivu-carousel-item{float:left;height:100%;min-height:1px;display:block}.ivu-carousel-arrow{border:none;outline:0;padding:0;margin:0;width:36px;height:36px;border-radius:50%;cursor:pointer;display:none;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);-webkit-transition:.2s;transition:.2s;background-color:rgba(31,45,61,.11);color:#fff;text-align:center;font-size:1em;font-family:inherit;line-height:inherit}.ivu-carousel-arrow:hover{background-color:rgba(31,45,61,.5)}.ivu-carousel-arrow>*{vertical-align:baseline}.ivu-carousel-arrow.left{left:16px}.ivu-carousel-arrow.right{right:16px}.ivu-carousel-arrow-always{display:inherit}.ivu-carousel-arrow-hover{display:inherit;opacity:0}.ivu-carousel:hover .ivu-carousel-arrow-hover{opacity:1}.ivu-carousel-dots{z-index:10;display:none;position:relative;list-style:none;text-align:center;padding:0;width:100%;height:17px}.ivu-carousel-dots-inside{display:block;position:absolute;bottom:3px}.ivu-carousel-dots-outside{display:block;margin-top:3px}.ivu-carousel-dots li{position:relative;display:inline-block;vertical-align:top;text-align:center;margin:0 2px;padding:7px 0;cursor:pointer}.ivu-carousel-dots li button{border:0;cursor:pointer;background:#8391a5;opacity:.3;display:block;width:16px;height:3px;border-radius:1px;outline:0;font-size:0;color:transparent;-webkit-transition:all .5s;transition:all .5s}.ivu-carousel-dots li button.radius{width:6px;height:6px;border-radius:50%}.ivu-carousel-dots li:hover>button{opacity:.7}.ivu-carousel-dots li.ivu-carousel-active>button{opacity:1;width:24px}.ivu-carousel-dots li.ivu-carousel-active>button.radius{width:6px}.ivu-rate{display:inline-block;margin:0;padding:0;font-size:20px;vertical-align:middle;font-weight:400;font-style:normal}.ivu-rate-disabled .ivu-rate-star-content:before,.ivu-rate-disabled .ivu-rate-star:before{cursor:default}.ivu-rate-disabled .ivu-rate-star:hover{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.ivu-rate-star-full,.ivu-rate-star-zero{position:relative}.ivu-rate-star-first{position:absolute;left:0;top:0;width:50%;height:100%;overflow:hidden;opacity:0}.ivu-rate-star-first,.ivu-rate-star-second{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .3s ease;transition:all .3s ease;color:#e9e9e9;cursor:pointer}.ivu-rate-star-chart{display:inline-block;margin:0;padding:0;margin-right:8px;position:relative;font-family:Ionicons;-webkit-transition:all .3s ease;transition:all .3s ease}.ivu-rate-star-chart:hover{-webkit-transform:scale(1.1);-ms-transform:scale(1.1);transform:scale(1.1)}.ivu-rate-star-chart.ivu-rate-star-full .ivu-rate-star-first,.ivu-rate-star-chart.ivu-rate-star-full .ivu-rate-star-second{color:#f5a623}.ivu-rate-star-chart.ivu-rate-star-half .ivu-rate-star-first{opacity:1;color:#f5a623}.ivu-rate-star{display:inline-block;margin:0;padding:0;margin-right:8px;position:relative;font-family:Ionicons;-webkit-transition:all .3s ease;transition:all .3s ease}.ivu-rate-star:hover{-webkit-transform:scale(1.1);-ms-transform:scale(1.1);transform:scale(1.1)}.ivu-rate-star-content:before,.ivu-rate-star:before{color:#e9e9e9;cursor:pointer;content:"\F2BF";-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:block}.ivu-rate-star-content{position:absolute;left:0;top:0;width:50%;height:100%;overflow:hidden}.ivu-rate-star-content:before{color:transparent}.ivu-rate-star-full:before,.ivu-rate-star-half .ivu-rate-star-content:before{color:#f5a623}.ivu-rate-star-full:hover:before,.ivu-rate-star-half:hover .ivu-rate-star-content:before{color:#f7b84f}.ivu-rate-text{margin-left:8px;vertical-align:middle;display:inline-block;font-size:12px}.ivu-upload input[type=file]{display:none}.ivu-upload-list{margin-top:8px}.ivu-upload-list-file{padding:4px;color:#515a6e;border-radius:4px;-webkit-transition:background-color .2s ease-in-out;transition:background-color .2s ease-in-out;overflow:hidden;position:relative}.ivu-upload-list-file>span{cursor:pointer;-webkit-transition:color .2s ease-in-out;transition:color .2s ease-in-out}.ivu-upload-list-file>span i{display:inline-block;width:12px;height:12px;color:#515a6e;text-align:center}.ivu-upload-list-file:hover{background:#f3f3f3}.ivu-upload-list-file:hover>span{color:#2d8cf0}.ivu-upload-list-file:hover>span i{color:#515a6e}.ivu-upload-list-file:hover .ivu-upload-list-remove{opacity:1}.ivu-upload-list-remove{opacity:0;font-size:18px;cursor:pointer;float:right;margin-right:4px;color:#999;-webkit-transition:all .2s ease;transition:all .2s ease}.ivu-upload-list-remove:hover{color:#444}.ivu-upload-select{display:inline-block}.ivu-upload-drag{background:#fff;border:1px dashed #dcdee2;border-radius:4px;text-align:center;cursor:pointer;position:relative;overflow:hidden;-webkit-transition:border-color .2s ease;transition:border-color .2s ease}.ivu-upload-drag:hover{border:1px dashed #2d8cf0}.ivu-upload-dragOver{border:2px dashed #2d8cf0}.ivu-tree ul{list-style:none;margin:0;padding:0;font-size:12px}.ivu-tree ul.ivu-dropdown-menu{padding:0}.ivu-tree ul li{list-style:none;margin:8px 0;padding:0;white-space:nowrap;outline:0}.ivu-tree ul li.ivu-dropdown-item{margin:0;padding:7px 16px;white-space:nowrap}.ivu-tree li ul{margin:0;padding:0 0 0 18px}.ivu-tree-title{display:inline-block;margin:0;padding:0 4px;border-radius:3px;cursor:pointer;vertical-align:top;color:#515a6e;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.ivu-tree-title:hover{background-color:#eaf4fe}.ivu-tree-title-selected,.ivu-tree-title-selected:hover{background-color:#d5e8fc}.ivu-tree-arrow{cursor:pointer;width:12px;text-align:center;display:inline-block}.ivu-tree-arrow i{-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;font-size:14px;vertical-align:middle}.ivu-tree-arrow-open i{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.ivu-tree-arrow-disabled{cursor:not-allowed}.ivu-tree .ivu-checkbox-wrapper{margin-right:4px;margin-left:4px}.ivu-avatar{display:inline-block;text-align:center;background:#ccc;color:#fff;white-space:nowrap;position:relative;overflow:hidden;vertical-align:middle;width:32px;height:32px;line-height:32px;border-radius:16px}.ivu-avatar-image{background:0 0}.ivu-avatar .ivu-icon{position:relative;top:-1px}.ivu-avatar>*{line-height:32px}.ivu-avatar.ivu-avatar-icon{font-size:18px}.ivu-avatar-large{width:40px;height:40px;line-height:40px;border-radius:20px}.ivu-avatar-large>*{line-height:40px}.ivu-avatar-large.ivu-avatar-icon{font-size:24px}.ivu-avatar-large .ivu-icon{position:relative;top:-2px}.ivu-avatar-small{width:24px;height:24px;line-height:24px;border-radius:12px}.ivu-avatar-small>*{line-height:24px}.ivu-avatar-small.ivu-avatar-icon{font-size:14px}.ivu-avatar-square{border-radius:4px}.ivu-avatar>img{width:100%;height:100%}.ivu-color-picker{display:inline-block}.ivu-color-picker-hide{display:none}.ivu-color-picker-hide-drop{visibility:hidden}.ivu-color-picker-disabled{background-color:#f3f3f3;opacity:1;cursor:not-allowed;color:#ccc}.ivu-color-picker-disabled:hover{border-color:#e3e5e8}.ivu-color-picker>div:first-child:hover .ivu-input{border-color:#57a3f3}.ivu-color-picker>div:first-child.ivu-color-picker-disabled:hover .ivu-input{border-color:#e3e5e8}.ivu-color-picker .ivu-select-dropdown{padding:0}.ivu-color-picker-input.ivu-input:focus{-webkit-box-shadow:none;box-shadow:none}.ivu-color-picker-focused{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-color-picker-rel{line-height:0}.ivu-color-picker-color{width:18px;height:18px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);border-radius:2px;position:relative;top:2px}.ivu-color-picker-color div{width:100%;height:100%;-webkit-box-shadow:inset 0 0 0 1px rgba(0,0,0,.15);box-shadow:inset 0 0 0 1px rgba(0,0,0,.15);border-radius:2px}.ivu-color-picker-color-empty{background:#fff;overflow:hidden;text-align:center}.ivu-color-picker-color-empty i{font-size:18px;vertical-align:baseline}.ivu-color-picker-color-focused{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-color-picker-large .ivu-color-picker-color{width:20px;height:20px;top:1px}.ivu-color-picker-large .ivu-color-picker-color-empty i{font-size:20px}.ivu-color-picker-small .ivu-color-picker-color{width:14px;height:14px;top:3px}.ivu-color-picker-small .ivu-color-picker-color-empty i{font-size:14px}.ivu-color-picker-picker-wrapper{padding:8px 8px 0}.ivu-color-picker-picker-panel{width:240px;margin:0 auto;-webkit-box-sizing:initial;box-sizing:initial;position:relative}.ivu-color-picker-picker-alpha-slider,.ivu-color-picker-picker-hue-slider{height:10px;margin-top:8px;position:relative}.ivu-color-picker-picker-colors{margin-top:8px;overflow:hidden;border-radius:2px;-webkit-transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-color-picker-picker-colors:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-color-picker-picker-colors-wrapper{display:inline;width:20px;height:20px;float:left;position:relative}.ivu-color-picker-picker-colors-wrapper-color{outline:0;display:block;position:absolute;width:16px;height:16px;margin:2px;cursor:pointer;border-radius:2px;-webkit-box-shadow:inset 0 0 0 1px rgba(0,0,0,.15);box-shadow:inset 0 0 0 1px rgba(0,0,0,.15)}.ivu-color-picker-picker-colors-wrapper-circle{width:4px;height:4px;-webkit-box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;-webkit-transform:translate(-2px,-2px);-ms-transform:translate(-2px,-2px);transform:translate(-2px,-2px);position:absolute;top:10px;left:10px;cursor:pointer}.ivu-color-picker-picker .ivu-picker-confirm{margin-top:8px}.ivu-color-picker-saturation-wrapper{width:100%;padding-bottom:75%;position:relative;-webkit-transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-color-picker-saturation-wrapper:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-color-picker-saturation,.ivu-color-picker-saturation--black,.ivu-color-picker-saturation--white{cursor:pointer;position:absolute;top:0;left:0;right:0;bottom:0}.ivu-color-picker-saturation--white{background:-webkit-gradient(linear,left top,right top,from(#fff),to(rgba(255,255,255,0)));background:linear-gradient(to right,#fff,rgba(255,255,255,0))}.ivu-color-picker-saturation--black{background:-webkit-gradient(linear,left bottom,left top,from(#000),to(rgba(0,0,0,0)));background:linear-gradient(to top,#000,rgba(0,0,0,0))}.ivu-color-picker-saturation-pointer{cursor:pointer;position:absolute}.ivu-color-picker-saturation-circle{width:4px;height:4px;-webkit-box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;-webkit-transform:translate(-2px,-2px);-ms-transform:translate(-2px,-2px);transform:translate(-2px,-2px)}.ivu-color-picker-hue{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:2px;background:-webkit-gradient(linear,left top,right top,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to right,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%);-webkit-transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-color-picker-hue:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-color-picker-hue-container{cursor:pointer;margin:0 2px;position:relative;height:100%}.ivu-color-picker-hue-pointer{z-index:2;position:absolute}.ivu-color-picker-hue-picker{cursor:pointer;margin-top:1px;width:4px;border-radius:1px;height:8px;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);background:#fff;-webkit-transform:translateX(-2px);-ms-transform:translateX(-2px);transform:translateX(-2px)}.ivu-color-picker-alpha{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:2px;-webkit-transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,-webkit-box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out;transition:border .2s ease-in-out,box-shadow .2s ease-in-out,-webkit-box-shadow .2s ease-in-out}.ivu-color-picker-alpha:focus{border-color:#57a3f3;outline:0;-webkit-box-shadow:0 0 0 2px rgba(45,140,240,.2);box-shadow:0 0 0 2px rgba(45,140,240,.2)}.ivu-color-picker-alpha-checkboard-wrap{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden;border-radius:2px}.ivu-color-picker-alpha-checkerboard{position:absolute;top:0;right:0;bottom:0;left:0;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.ivu-color-picker-alpha-gradient{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:2px}.ivu-color-picker-alpha-container{cursor:pointer;position:relative;z-index:2;height:100%;margin:0 3px}.ivu-color-picker-alpha-pointer{z-index:2;position:absolute}.ivu-color-picker-alpha-picker{cursor:pointer;width:4px;border-radius:1px;height:8px;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);background:#fff;margin-top:1px;-webkit-transform:translateX(-2px);-ms-transform:translateX(-2px);transform:translateX(-2px)}.ivu-color-picker-confirm{margin-top:8px;position:relative;border-top:1px solid #e8eaec;text-align:right;padding:8px;clear:both}.ivu-color-picker-confirm-color{position:absolute;top:11px;left:8px}.ivu-color-picker-confirm-color-editable{top:8px}.ivu-auto-complete .ivu-select-not-found{display:none}.ivu-auto-complete .ivu-icon-ios-close{display:none}.ivu-auto-complete:hover .ivu-icon-ios-close{display:inline-block}.ivu-auto-complete.ivu-select-dropdown{max-height:none}.ivu-divider{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;font-size:14px;line-height:1.5;color:#515a6e;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;list-style:none;background:#e8eaec}.ivu-divider,.ivu-divider-vertical{margin:0 8px;display:inline-block;height:.9em;width:1px;vertical-align:middle;position:relative;top:-.06em}.ivu-divider-horizontal{display:block;height:1px;width:100%;min-width:100%;margin:24px 0;clear:both}.ivu-divider-horizontal.ivu-divider-with-text-center,.ivu-divider-horizontal.ivu-divider-with-text-left,.ivu-divider-horizontal.ivu-divider-with-text-right{display:table;white-space:nowrap;text-align:center;background:0 0;font-weight:500;color:#17233d;font-size:16px;margin:16px 0}.ivu-divider-horizontal.ivu-divider-with-text-center:after,.ivu-divider-horizontal.ivu-divider-with-text-center:before,.ivu-divider-horizontal.ivu-divider-with-text-left:after,.ivu-divider-horizontal.ivu-divider-with-text-left:before,.ivu-divider-horizontal.ivu-divider-with-text-right:after,.ivu-divider-horizontal.ivu-divider-with-text-right:before{content:'';display:table-cell;position:relative;top:50%;width:50%;border-top:1px solid #e8eaec;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%)}.ivu-divider-horizontal.ivu-divider-small.ivu-divider-with-text-center,.ivu-divider-horizontal.ivu-divider-small.ivu-divider-with-text-left,.ivu-divider-horizontal.ivu-divider-small.ivu-divider-with-text-right{font-size:14px;margin:8px 0}.ivu-divider-horizontal.ivu-divider-with-text-left .ivu-divider-inner-text,.ivu-divider-horizontal.ivu-divider-with-text-right .ivu-divider-inner-text{display:inline-block;padding:0 10px}.ivu-divider-horizontal.ivu-divider-with-text-left:before{top:50%;width:5%}.ivu-divider-horizontal.ivu-divider-with-text-left:after{top:50%;width:95%}.ivu-divider-horizontal.ivu-divider-with-text-right:before{top:50%;width:95%}.ivu-divider-horizontal.ivu-divider-with-text-right:after{top:50%;width:5%}.ivu-divider-inner-text{display:inline-block;padding:0 24px}.ivu-divider-dashed{background:0 0;border-top:1px dashed #e8eaec}.ivu-divider-horizontal.ivu-divider-with-text-left.ivu-divider-dashed,.ivu-divider-horizontal.ivu-divider-with-text-right.ivu-divider-dashed,.ivu-divider-horizontal.ivu-divider-with-text.ivu-divider-dashed{border-top:0}.ivu-divider-horizontal.ivu-divider-with-text-left.ivu-divider-dashed:after,.ivu-divider-horizontal.ivu-divider-with-text-left.ivu-divider-dashed:before,.ivu-divider-horizontal.ivu-divider-with-text-right.ivu-divider-dashed:after,.ivu-divider-horizontal.ivu-divider-with-text-right.ivu-divider-dashed:before,.ivu-divider-horizontal.ivu-divider-with-text.ivu-divider-dashed:after,.ivu-divider-horizontal.ivu-divider-with-text.ivu-divider-dashed:before{border-style:dashed none none}.ivu-anchor{position:relative;padding-left:2px}.ivu-anchor-wrapper{overflow:auto;padding-left:4px;margin-left:-4px}.ivu-anchor-ink{position:absolute;height:100%;left:0;top:0}.ivu-anchor-ink:before{content:' ';position:relative;width:2px;height:100%;display:block;background-color:#e8eaec;margin:0 auto}.ivu-anchor-ink-ball{display:inline-block;position:absolute;width:8px;height:8px;border-radius:50%;border:2px solid #2d8cf0;background-color:#fff;left:50%;-webkit-transition:top .2s ease-in-out;transition:top .2s ease-in-out;-webkit-transform:translate(-50%,2px);-ms-transform:translate(-50%,2px);transform:translate(-50%,2px)}.ivu-anchor.fixed .ivu-anchor-ink .ivu-anchor-ink-ball{display:none}.ivu-anchor-link{padding:8px 0 8px 16px;line-height:1}.ivu-anchor-link-title{display:block;position:relative;-webkit-transition:all .3s;transition:all .3s;color:#515a6e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:8px}.ivu-anchor-link-title:only-child{margin-bottom:0}.ivu-anchor-link-active>.ivu-anchor-link-title{color:#2d8cf0}.ivu-anchor-link .ivu-anchor-link{padding-top:6px;padding-bottom:6px}.ivu-time-with-hash{cursor:pointer}.ivu-time-with-hash:hover{text-decoration:underline}.ivu-cell{position:relative;overflow:hidden}.ivu-cell-link,.ivu-cell-link:active,.ivu-cell-link:hover{color:inherit}.ivu-cell-icon{display:inline-block;margin-right:4px;font-size:14px;vertical-align:middle}.ivu-cell-icon:empty{display:none}.ivu-cell-main{display:inline-block;vertical-align:middle}.ivu-cell-title{line-height:24px;font-size:14px}.ivu-cell-label{line-height:1.2;font-size:12px;color:#808695}.ivu-cell-selected .ivu-cell-label{color:inherit}.ivu-cell-selected,.ivu-cell.ivu-cell-selected:hover{background:#f0faff}.ivu-cell-footer{display:inline-block;position:absolute;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);top:50%;right:16px;color:#515a6e}.ivu-cell-with-link .ivu-cell-footer{right:32px}.ivu-cell-selected .ivu-cell-footer{color:inherit}.ivu-cell-arrow{display:inline-block;position:absolute;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);top:50%;right:16px;font-size:14px}.ivu-cell:focus{background:#f3f3f3;outline:0}.ivu-cell-selected:focus{background:rgba(40,123,211,.91)}.ivu-cell{margin:0;line-height:normal;padding:7px 16px;clear:both;color:#515a6e;font-size:12px!important;white-space:nowrap;list-style:none;cursor:pointer;-webkit-transition:background .2s ease-in-out;transition:background .2s ease-in-out}.ivu-cell:hover{background:#f3f3f3}.ivu-cell-focus{background:#f3f3f3}.ivu-cell-disabled{color:#c5c8ce;cursor:not-allowed}.ivu-cell-disabled:hover{color:#c5c8ce;background-color:#fff;cursor:not-allowed}.ivu-cell-selected,.ivu-cell-selected:hover{color:#2d8cf0}.ivu-cell-divided{margin-top:5px;border-top:1px solid #e8eaec}.ivu-cell-divided:before{content:'';height:5px;display:block;margin:0 -16px;background-color:#fff;position:relative;top:-7px}.ivu-cell-large .ivu-cell{padding:7px 16px 8px;font-size:14px!important}@-moz-document url-prefix(){.ivu-cell{white-space:normal}}.ivu-drawer{width:auto;height:100%;position:fixed;top:0}.ivu-drawer-inner{position:absolute}.ivu-drawer-left{left:0}.ivu-drawer-right{right:0}.ivu-drawer-hidden{display:none!important}.ivu-drawer-wrap{position:fixed;overflow:auto;top:0;right:0;bottom:0;left:0;z-index:1000;-webkit-overflow-scrolling:touch;outline:0}.ivu-drawer-wrap-inner{position:absolute;overflow:hidden}.ivu-drawer-wrap-dragging{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ivu-drawer-wrap *{-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-tap-highlight-color:transparent}.ivu-drawer-mask{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(55,55,55,.6);height:100%;z-index:1000}.ivu-drawer-mask-hidden{display:none}.ivu-drawer-mask-inner{position:absolute}.ivu-drawer-content{width:100%;height:100%;position:absolute;top:0;bottom:0;background-color:#fff;border:0;background-clip:padding-box;-webkit-box-shadow:0 4px 12px rgba(0,0,0,.15);box-shadow:0 4px 12px rgba(0,0,0,.15)}.ivu-drawer-content-no-mask{pointer-events:auto}.ivu-drawer-header{border-bottom:1px solid #e8eaec;padding:14px 16px;line-height:1}.ivu-drawer-header p,.ivu-drawer-header-inner{display:inline-block;width:100%;height:20px;line-height:20px;font-size:14px;color:#17233d;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ivu-drawer-header p i,.ivu-drawer-header p span{vertical-align:middle}.ivu-drawer-close{z-index:1;font-size:12px;position:absolute;right:8px;top:8px;overflow:hidden;cursor:pointer}.ivu-drawer-close .ivu-icon-ios-close{font-size:31px;color:#999;-webkit-transition:color .2s ease;transition:color .2s ease;position:relative;top:1px}.ivu-drawer-close .ivu-icon-ios-close:hover{color:#444}.ivu-drawer-body{width:100%;height:calc(100% - 51px);padding:16px;font-size:12px;line-height:1.5;word-wrap:break-word;position:absolute;overflow:auto}.ivu-drawer-no-header .ivu-drawer-body{height:100%}.ivu-drawer-no-mask{pointer-events:none}.ivu-drawer-no-mask .ivu-drawer-drag{pointer-events:auto}.ivu-drawer-drag{top:0;height:100%;width:0;position:absolute}.ivu-drawer-drag-left{right:0}.ivu-drawer-drag-move-trigger{width:8px;height:100px;line-height:100px;position:absolute;top:50%;background:#f3f3f3;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border-radius:4px/6px;-webkit-box-shadow:0 0 1px 1px rgba(0,0,0,.2);box-shadow:0 0 1px 1px rgba(0,0,0,.2);cursor:col-resize}.ivu-drawer-drag-move-trigger-point{display:inline-block;width:50%;-webkit-transform:translateX(50%);-ms-transform:translateX(50%);transform:translateX(50%)}.ivu-drawer-drag-move-trigger-point i{display:block;border-bottom:1px solid silver;padding-bottom:2px}
================================================
FILE: demo-codegen/src/main/resources/template/Controller.java.vm
================================================
package ${package}.${moduleName}.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import ${package}.${moduleName}.common.R;
import ${package}.${moduleName}.entity.${className};
import ${package}.${moduleName}.service.${className}Service;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import lombok.extern.slf4j.Slf4j;
/**
*
* ${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}));
}
}
================================================
FILE: demo-codegen/src/main/resources/template/Entity.java.vm
================================================
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
}
================================================
FILE: demo-codegen/src/main/resources/template/Mapper.java.vm
================================================
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}> {
}
================================================
FILE: demo-codegen/src/main/resources/template/Mapper.xml.vm
================================================
#foreach($column in $columns)
#if($column.lowerAttrName==$pk.lowerAttrName)
#else
#end
#end
================================================
FILE: demo-codegen/src/main/resources/template/Service.java.vm
================================================
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}> {
}
================================================
FILE: demo-codegen/src/main/resources/template/ServiceImpl.java.vm
================================================
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 {
}
================================================
FILE: demo-codegen/src/main/resources/template/api.js.vm
================================================
import request from '@/router/axios'
/**
* 分页查询${comments}
* @param query 分页查询条件
*/
export function fetchList(query) {
return request({
url: '/${moduleName}/${pathName}',
method: 'get',
params: query
})
}
/**
* 新增${comments}
* @param obj ${comments}
*/
export function addObj(obj) {
return request({
url: '/${moduleName}/${pathName}',
method: 'post',
data: obj
})
}
/**
* 通过id查询${comments}
* @param id 主键
*/
export function getObj(id) {
return request({
url: '/${moduleName}/${pathName}/' + id,
method: 'get'
})
}
/**
* 通过id删除${comments}
* @param id 主键
*/
export function delObj(id) {
return request({
url: '/${moduleName}/${pathName}/' + id,
method: 'delete'
})
}
/**
* 修改${comments}
* @param id 主键
* @param obj ${comments}
*/
export function putObj(id, obj) {
return request({
url: '/${moduleName}/${pathName}/' + id,
method: 'put',
data: obj
})
}
================================================
FILE: demo-codegen/src/test/java/com/xkcoding/codegen/CodeGenServiceTest.java
================================================
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);
}
}
================================================
FILE: demo-codegen/src/test/java/com/xkcoding/codegen/SpringBootDemoCodegenApplicationTests.java
================================================
package com.xkcoding.codegen;
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 SpringBootDemoCodegenApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-docker/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-docker/Dockerfile
================================================
# 基础镜像
FROM openjdk:8-jdk-alpine
# 作者信息
MAINTAINER "Yangkai.Shen 237497819@qq.com"
# 添加一个存储空间
VOLUME /tmp
# 暴露8080端口
EXPOSE 8080
# 添加变量,如果使用dockerfile-maven-plugin,则会自动替换这里的变量内容
ARG JAR_FILE=target/spring-boot-demo-docker.jar
# 往容器中添加jar包
ADD ${JAR_FILE} app.jar
# 启动镜像自动运行程序
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/urandom","-jar","/app.jar"]
================================================
FILE: demo-docker/README.md
================================================
# spring-boot-demo-docker
> 本 demo 主要演示了如何容器化一个 Spring Boot 项目。通过 `Dockerfile` 的方式打包成一个 images 。
## Dockerfile
```dockerfile
# 基础镜像
FROM openjdk:8-jdk-alpine
# 作者信息
MAINTAINER "Yangkai.Shen 237497819@qq.com"
# 添加一个存储空间
VOLUME /tmp
# 暴露8080端口
EXPOSE 8080
# 添加变量,如果使用dockerfile-maven-plugin,则会自动替换这里的变量内容
ARG JAR_FILE=target/spring-boot-demo-docker.jar
# 往容器中添加jar包
ADD ${JAR_FILE} app.jar
# 启动镜像自动运行程序
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/urandom","-jar","/app.jar"]
```
## 打包方式
### 手动打包
1. 前往 Dockerfile 目录,打开命令行执行
```bash
$ docker build -t spring-boot-demo-docker .
```
2. 查看生成镜像
```bash
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-boot-demo-docker latest bc29a29ffca0 2 hours ago 119MB
openjdk 8-jdk-alpine 97bc1352afde 5 weeks ago 103MB
```
3. 运行
```bash
$ docker run -d -p 9090:8080 spring-boot-demo-docker
```
### 使用 maven 插件打包
1. pom.xml 中添加插件
2. ```xml
1.4.9
com.spotify
dockerfile-maven-plugin
${dockerfile-version}
${project.build.finalName}
${project.version}
target/${project.build.finalName}.jar
default
package
build
```
2. 执行mvn打包命令,因为插件中 `execution` 节点配置了 package,所以会在打包的时候自动执行 build 命令。
```bash
$ mvn clean package -Dmaven.test.skip=true
```
3. 查看镜像
```bash
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-boot-demo-docker 1.0.0-SNAPSHOT bc29a29ffca0 2 hours ago 119MB
openjdk 8-jdk-alpine 97bc1352afde 5 weeks ago 103MB
```
4. 运行
```bash
$ docker run -d -p 9090:8080 spring-boot-demo-docker:1.0.0-SNAPSHOT
```
## 参考
- docker 官方文档:https://docs.docker.com/
- Dockerfile 命令,参考文档:https://docs.docker.com/engine/reference/builder/
- maven插件使用,参考地址:https://github.com/spotify/dockerfile-maven
================================================
FILE: demo-docker/pom.xml
================================================
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
================================================
FILE: demo-docker/src/main/java/com/xkcoding/docker/SpringBootDemoDockerApplication.java
================================================
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);
}
}
================================================
FILE: demo-docker/src/main/java/com/xkcoding/docker/controller/HelloController.java
================================================
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!";
}
}
================================================
FILE: demo-docker/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
================================================
FILE: demo-docker/src/test/java/com/xkcoding/docker/SpringBootDemoDockerApplicationTests.java
================================================
package com.xkcoding.docker;
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 SpringBootDemoDockerApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-dubbo/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-dubbo/README.md
================================================
# spring-boot-demo-dubbo
> 此 demo 主要演示了 Spring Boot 如何集成 Dubbo,demo 分了3个module,分别为公共模块 `spring-boot-demo-dubbo-common`、服务提供方`spring-boot-demo-dubbo-provider`、服务调用方`spring-boot-demo-dubbo-consumer`
## 注意
本例注册中心使用的是 zookeeper,作者编写本demo时,采用docker方式运行 zookeeper
1. 下载镜像:`docker pull wurstmeister/zookeeper`
2. 运行容器:`docker run -d -p 2181:2181 -p 2888:2888 -p 2222:22 -p 3888:3888 --name zk wurstmeister/zookeeper`
3. 停止容器:`docker stop zk`
4. 启动容器:`docker start zk`
## 运行步骤
1. 进入服务提供方 `spring-boot-demo-dubbo-provider` 目录,运行 `SpringBootDemoDubboProviderApplication.java`
2. 进入服务调用方 `spring-boot-demo-dubbo-consumer` 目录,运行 `SpringBootDemoDubboConsumerApplication.java`
3. 打开浏览器输入 http://localhost:8080/demo/sayHello ,观察浏览器输出,以及服务提供方和服务调用方的控制台输出日志情况
## pom.xml
```xml
4.0.0
spring-boot-demo-dubbo
1.0.0-SNAPSHOT
spring-boot-demo-dubbo-common
spring-boot-demo-dubbo-provider
spring-boot-demo-dubbo-consumer
pom
spring-boot-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
org.springframework.boot
spring-boot-maven-plugin
```
## 参考
1. dubbo 官网:http://dubbo.apache.org/zh-cn/
2. [超详细,新手都能看懂 !使用SpringBoot+Dubbo 搭建一个简单的分布式服务](https://segmentfault.com/a/1190000017178722#articleHeader20)
================================================
FILE: demo-dubbo/dubbo-common/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-dubbo/dubbo-common/README.md
================================================
# 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);
}
```
================================================
FILE: demo-dubbo/dubbo-common/pom.xml
================================================
demo-dubbo
com.xkcoding
1.0.0-SNAPSHOT
4.0.0
dubbo-common
UTF-8
UTF-8
1.8
dubbo-common
================================================
FILE: demo-dubbo/dubbo-common/src/main/java/com/xkcoding/dubbo/common/service/HelloService.java
================================================
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);
}
================================================
FILE: demo-dubbo/dubbo-consumer/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-dubbo/dubbo-consumer/README.md
================================================
# 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);
}
}
```
================================================
FILE: demo-dubbo/dubbo-consumer/pom.xml
================================================
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
================================================
FILE: demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplication.java
================================================
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);
}
}
================================================
FILE: demo-dubbo/dubbo-consumer/src/main/java/com/xkcoding/dubbo/consumer/controller/HelloController.java
================================================
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);
}
}
================================================
FILE: demo-dubbo/dubbo-consumer/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
spring:
dubbo:
application:
name: spring-boot-demo-dubbo-consumer
registry: zookeeper://127.0.0.1:2181
================================================
FILE: demo-dubbo/dubbo-consumer/src/test/java/com/xkcoding/dubbo/consumer/SpringBootDemoDubboConsumerApplicationTests.java
================================================
package com.xkcoding.dubbo.consumer;
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 SpringBootDemoDubboConsumerApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-dubbo/dubbo-provider/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-dubbo/dubbo-provider/README.md
================================================
# 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;
}
}
```
================================================
FILE: demo-dubbo/dubbo-provider/pom.xml
================================================
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
================================================
FILE: demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplication.java
================================================
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);
}
}
================================================
FILE: demo-dubbo/dubbo-provider/src/main/java/com/xkcoding/dubbo/provider/service/HelloServiceImpl.java
================================================
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;
}
}
================================================
FILE: demo-dubbo/dubbo-provider/src/main/resources/application.yml
================================================
server:
port: 9090
servlet:
context-path: /demo
spring:
dubbo:
application:
name: spring-boot-demo-dubbo-provider
registry: zookeeper://localhost:2181
================================================
FILE: demo-dubbo/dubbo-provider/src/test/java/com/xkcoding/dubbo/provider/SpringBootDemoDubboProviderApplicationTests.java
================================================
package com.xkcoding.dubbo.provider;
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 SpringBootDemoDubboProviderApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-dubbo/pom.xml
================================================
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
================================================
FILE: demo-dynamic-datasource/.gitignore
================================================
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
================================================
FILE: demo-dynamic-datasource/README.md
================================================
# 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. 测试
启动项目,可以看到控制台读取到数据库已配置的数据源信息

通过 PostMan 等工具测试
- 默认数据源查询

- 根据数据源id为1的数据源查询

- 根据数据源id为2的数据源查询

- 可以通过测试数据源的`增加/删除`,再去查询对应数据源的数据
> 删除数据源:
>
> - 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 {
}
```
完结,撒花✿✿ヽ(°▽°)ノ✿
================================================
FILE: demo-dynamic-datasource/db/init.sql
================================================
CREATE TABLE IF NOT EXISTS `datasource_config`
(
`id` bigint(13) NOT NULL AUTO_INCREMENT COMMENT '主键',
`host` varchar(255) NOT NULL COMMENT '数据库地址',
`port` int(6) NOT NULL COMMENT '数据库端口',
`username` varchar(100) NOT NULL COMMENT '数据库用户名',
`password` varchar(100) NOT NULL COMMENT '数据库密码',
`database` varchar(100) DEFAULT 0 COMMENT '数据库名称',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 COMMENT ='数据源配置表';
INSERT INTO `datasource_config`(`id`, `host`, `port`, `username`, `password`, `database`)
VALUES (1, '127.0.01', 3306, 'root', 'root', 'test');
INSERT INTO `datasource_config`(`id`, `host`, `port`, `username`, `password`, `database`)
VALUES (2, '192.168.239.4', 3306, 'dmcp', 'Dmcp321!', 'test');
================================================
FILE: demo-dynamic-datasource/db/user.sql
================================================
CREATE TABLE IF NOT EXISTS `test_user`
(
`id` bigint(13) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) NOT NULL COMMENT '姓名',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 COMMENT ='用户表';
-- 默认数据库插入如下 SQL
INSERT INTO `test_user`(`id`, `name`)
values (1, '默认数据库用户1');
INSERT INTO `test_user`(`id`, `name`)
values (2, '默认数据库用户2');
-- 测试库1插入如下SQL
INSERT INTO `test_user`(`id`, `name`)
values (1, '测试库1用户1');
INSERT INTO `test_user`(`id`, `name`)
values (2, '测试库1用户2');
-- 测试库2插入如下SQL
INSERT INTO `test_user`(`id`, `name`)
values (1, '测试库2用户1');
INSERT INTO `test_user`(`id`, `name`)
values (2, '测试库2用户2');
================================================
FILE: demo-dynamic-datasource/pom.xml
================================================
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
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplication.java
================================================
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));
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/annotation/DefaultDatasource.java
================================================
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 {
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/aspect/DatasourceSelectorAspect.java
================================================
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();
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/DatasourceConfiguration.java
================================================
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();
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MyMapper.java
================================================
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 {
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/config/MybatisConfiguration.java
================================================
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();
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/DatasourceConfigController.java
================================================
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);
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/controller/UserController.java
================================================
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();
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigCache.java
================================================
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);
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceConfigContextHolder.java
================================================
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);
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceHolder.java
================================================
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);
}
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceManager.java
================================================
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();
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DatasourceScheduler.java
================================================
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);
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/datasource/DynamicDataSource.java
================================================
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;
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/DatasourceConfigMapper.java
================================================
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 {
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/mapper/UserMapper.java
================================================
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 {
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/DatasourceConfig.java
================================================
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);
}
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/model/User.java
================================================
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;
}
================================================
FILE: demo-dynamic-datasource/src/main/java/com/xkcoding/dynamic/datasource/utils/SpringUtil.java
================================================
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();
}
}
================================================
FILE: demo-dynamic-datasource/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
================================================
FILE: demo-dynamic-datasource/src/test/java/com/xkcoding/dynamic/datasource/SpringBootDemoDynamicDatasourceApplicationTests.java
================================================
package com.xkcoding.dynamic.datasource;
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 SpringBootDemoDynamicDatasourceApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-elasticsearch/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-elasticsearch/README.md
================================================
# 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/
================================================
FILE: demo-elasticsearch/pom.xml
================================================
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
================================================
FILE: demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplication.java
================================================
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);
}
}
================================================
FILE: demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/constants/EsConsts.java
================================================
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";
}
================================================
FILE: demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/model/Person.java
================================================
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;
}
================================================
FILE: demo-elasticsearch/src/main/java/com/xkcoding/elasticsearch/repository/PersonRepository.java
================================================
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);
}
================================================
FILE: demo-elasticsearch/src/main/resources/application.yml
================================================
spring:
data:
elasticsearch:
cluster-name: docker-cluster
cluster-nodes: localhost:9300
================================================
FILE: demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/SpringBootDemoElasticsearchApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/repository/PersonRepositoryTest.java
================================================
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);
}
}
}
================================================
FILE: demo-elasticsearch/src/test/java/com/xkcoding/elasticsearch/template/TemplateTest.java
================================================
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);
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-elasticsearch-rest-high-level-client/README.md
================================================
# 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
================================================
FILE: demo-elasticsearch-rest-high-level-client/pom.xml
================================================
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
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/ElasticsearchApplication.java
================================================
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);
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/Result.java
================================================
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);
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/common/ResultCode.java
================================================
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;
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchAutoConfiguration.java
================================================
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);
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/config/ElasticsearchProperties.java
================================================
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;
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/contants/ElasticsearchConstant.java
================================================
package com.xkcoding.elasticsearch.contants;
/**
* ElasticsearchConstant
*
* @author fxbin
* @version v1.0
* @since 2019-09-15 23:03
*/
public interface ElasticsearchConstant {
/**
* 索引名称
*/
String INDEX_NAME = "person";
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/exception/ElasticsearchException.java
================================================
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);
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/model/Person.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;
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/PersonService.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
*
* @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);
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/base/BaseElasticsearchService.java
================================================
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;
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/java/com/xkcoding/elasticsearch/service/impl/PersonServiceImpl.java
================================================
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;
}
}
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/main/resources/application.yml
================================================
demo:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 20.20.0.27:9200
index:
number-of-replicas: 0
number-of-shards: 3
================================================
FILE: demo-elasticsearch-rest-high-level-client/src/test/java/com/xkcoding/elasticsearch/ElasticsearchApplicationTests.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);
}
}
================================================
FILE: demo-email/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-email/README.md
================================================
# 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
================================================
FILE: demo-email/pom.xml
================================================
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
================================================
FILE: demo-email/src/main/java/com/xkcoding/email/SpringBootDemoEmailApplication.java
================================================
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);
}
}
================================================
FILE: demo-email/src/main/java/com/xkcoding/email/service/MailService.java
================================================
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;
}
================================================
FILE: demo-email/src/main/java/com/xkcoding/email/service/impl/MailServiceImpl.java
================================================
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);
}
}
================================================
FILE: demo-email/src/main/resources/application.yml
================================================
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
================================================
FILE: demo-email/src/main/resources/email/test.html
================================================
SpringBootDemo(入门SpringBoot的首选Demo)
欢迎使用 - Powered By
如果对你有帮助,请任意打赏
================================================
FILE: demo-email/src/main/resources/templates/welcome.html
================================================
SpringBootDemo(入门SpringBoot的首选Demo)
欢迎使用 - Powered By
如果对你有帮助,请任意打赏
================================================
FILE: demo-email/src/test/java/com/xkcoding/email/PasswordTest.java
================================================
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);
}
}
================================================
FILE: demo-email/src/test/java/com/xkcoding/email/SpringBootDemoEmailApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-email/src/test/java/com/xkcoding/email/service/MailServiceTest.java
================================================
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);
}
}
================================================
FILE: demo-exception-handler/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-exception-handler/README.md
================================================
# 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
统一页面异常处理
统一页面异常处理
```
================================================
FILE: demo-exception-handler/pom.xml
================================================
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
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplication.java
================================================
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);
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/constant/Status.java
================================================
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;
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/controller/TestController.java
================================================
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);
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/BaseException.java
================================================
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;
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/JsonException.java
================================================
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);
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/exception/PageException.java
================================================
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);
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/handler/DemoExceptionHandler.java
================================================
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;
}
}
================================================
FILE: demo-exception-handler/src/main/java/com/xkcoding/exception/handler/model/ApiResponse.java
================================================
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);
}
}
================================================
FILE: demo-exception-handler/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
spring:
thymeleaf:
cache: false
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
================================================
FILE: demo-exception-handler/src/main/resources/templates/error.html
================================================
统一页面异常处理
统一页面异常处理
================================================
FILE: demo-exception-handler/src/test/java/com/xkcoding/exception/handler/SpringBootDemoExceptionHandlerApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-flyway/.gitignore
================================================
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
================================================
FILE: demo-flyway/README.md
================================================
# spring-boot-demo-flyway
> 本 demo 演示了 Spring Boot 如何使用 Flyway 去初始化项目数据库,同时支持数据库脚本的版本控制。
## 1. 添加依赖
- Flyway 依赖
```xml
org.flywaydb
flyway-core
```
- 初始化表结构,需要操作数据库,因此引入数据库驱动以及数据源依赖(这里用 spring-boot-starter-data-jdbc)
```xml
org.springframework.boot
spring-boot-starter-data-jdbc
mysql
mysql-connector-java
runtime
```
## 2. Flyway 知识补充
1. Flyway 默认会去读取 `classpath:db/migration`,可以通过 `spring.flyway.locations` 去指定自定义路径,多个路径使用半角英文逗号分隔,内部资源使用 `classpath:`,外部资源使用 `file:`
2. 如果项目初期没有数据库文件,但是又引用了 Flyway,那么在项目启动的时候,Flyway 会去检查是否存在 SQL 文件,此时你需要将这个检查关闭,`spring.flyway.check-location = false`
3. Flyway 会在项目初次启动的时候创建一张名为 `flyway_schema_history` 的表,在这张表里记录数据库脚本执行的历史记录,当然,你可以通过 `spring.flyway.table` 去修改这个值
4. Flyway 执行的 SQL 脚本必须遵循一种命名规则,`V__.sql` 首先是 `V` ,然后是版本号,如果版本号有多个数字,使用`_`分隔,比如`1_0`、`1_1`,版本号的后面是 2 个下划线,最后是 SQL 脚本的名称。
**这里需要注意:V 开头的只会执行一次,下次项目启动不会执行,也不可以修改原始文件,否则项目启动会报错,如果需要对 V 开头的脚本做修改,需要清空`flyway_schema_history`表,如果有个 SQL 脚本需要在每次启动的时候都执行,那么将 V 改为 `R` 开头即可**
5. Flyway 默认情况下会去清空原始库,再重新执行 SQL 脚本,这在生产环境下是不可取的,因此需要将这个配置关闭,`spring.flyway.clean-disabled = true`
## 3. application.yml 配置
> 贴出我的 application.yml 配置
```yaml
spring:
flyway:
enabled: true
# 迁移前校验 SQL 文件是否存在问题
validate-on-migrate: true
# 生产环境一定要关闭
clean-disabled: true
# 校验路径下是否存在 SQL 文件
check-location: false
# 最开始已经存在表结构,且不存在 flyway_schema_history 表时,需要设置为 true
baseline-on-migrate: true
# 基础版本 0
baseline-version: 0
datasource:
url: jdbc:mysql://127.0.0.1:3306/flyway-test?useSSL=false
username: root
password: root
type: com.zaxxer.hikari.HikariDataSource
```
## 4. 测试
### 4.1. 测试 1.0 版本的 SQL 脚本
创建 `V1_0__INIT.sql`
```mysql
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(32) NOT NULL COMMENT '用户名',
`password` varchar(32) NOT NULL COMMENT '加密后的密码',
`salt` varchar(32) NOT NULL COMMENT '加密使用的盐',
`email` varchar(32) NOT NULL COMMENT '邮箱',
`phone_number` varchar(15) NOT NULL COMMENT '手机号码',
`status` int(2) NOT NULL DEFAULT '1' COMMENT '状态,-1:逻辑删除,0:禁用,1:启用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`last_login_time` datetime DEFAULT NULL COMMENT '上次登录时间',
`last_update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `phone_number` (`phone_number`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='1.0-用户表';
```
启动项目,可以看到日志输出:
```java
2020-03-05 10:48:37.799 INFO 3351 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.1 by Boxfuse
2020-03-05 10:48:37.802 INFO 3351 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-03-05 10:48:37.971 INFO 3351 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-03-05 10:48:37.974 INFO 3351 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://127.0.0.1:3306/flyway-test (MySQL 5.7)
2020-03-05 10:48:38.039 INFO 3351 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.015s)
2020-03-05 10:48:38.083 INFO 3351 --- [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table: `flyway-test`.`flyway_schema_history`
2020-03-05 10:48:38.143 INFO 3351 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `flyway-test`: << Empty Schema >>
2020-03-05 10:48:38.144 INFO 3351 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema `flyway-test` to version 1.0 - INIT
2020-03-05 10:48:38.156 WARN 3351 --- [ main] o.f.c.i.s.DefaultSqlScriptExecutor : DB: Unknown table 'flyway-test.t_user' (SQL State: 42S02 - Error Code: 1051)
2020-03-05 10:48:38.183 INFO 3351 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `flyway-test` (execution time 00:00.100s)
```
检查数据库,发现创建了 2 张表,一张是 Flyway 依赖的历史表,另一张就是我们的 `t_user` 表
查看下 `flyway-schema-history` 表
### 4.2. 测试 1.1 版本的 SQL 脚本
创建 `V1_1__ALTER.sql`
```mysql
ALTER TABLE t_user COMMENT = '用户 v1.1';
```
启动项目,可以看到日志输出:
```java
2020-03-05 10:59:02.279 INFO 3536 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.1 by Boxfuse
2020-03-05 10:59:02.282 INFO 3536 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-03-05 10:59:02.442 INFO 3536 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-03-05 10:59:02.445 INFO 3536 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://127.0.0.1:3306/flyway-test (MySQL 5.7)
2020-03-05 10:59:02.530 INFO 3536 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 2 migrations (execution time 00:00.018s)
2020-03-05 10:59:02.538 INFO 3536 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema `flyway-test`: 1.0
2020-03-05 10:59:02.538 INFO 3536 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema `flyway-test` to version 1.1 - ALTER
2020-03-05 10:59:02.564 INFO 3536 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `flyway-test` (execution time 00:00.029s)
```
检查数据库,可以发现 `t_user` 表的注释已经更新
查看下 `flyway-schema-history` 表
## 参考
1. [Spring Boot 官方文档 - Migration 章节](https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#howto-execute-flyway-database-migrations-on-startup)
2. [Flyway 官方文档](https://flywaydb.org/documentation/)
================================================
FILE: demo-flyway/pom.xml
================================================
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
================================================
FILE: demo-flyway/src/main/java/com/xkcoding/flyway/SpringBootDemoFlywayApplication.java
================================================
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);
}
}
================================================
FILE: demo-flyway/src/main/resources/application.yml
================================================
spring:
flyway:
enabled: true
# 迁移前校验 SQL 文件是否存在问题
validate-on-migrate: true
# 生产环境一定要关闭
clean-disabled: true
# 校验路径下是否存在 SQL 文件
check-location: false
# 最开始已经存在表结构,且不存在 flyway_schema_history 表时,需要设置为 true
baseline-on-migrate: true
# 基础版本 0
baseline-version: 0
datasource:
url: jdbc:mysql://127.0.0.1:3306/flyway-test?useSSL=false
username: root
password: root
type: com.zaxxer.hikari.HikariDataSource
================================================
FILE: demo-flyway/src/main/resources/db/migration/V1_0__INIT.sql
================================================
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(32) NOT NULL COMMENT '用户名',
`password` varchar(32) NOT NULL COMMENT '加密后的密码',
`salt` varchar(32) NOT NULL COMMENT '加密使用的盐',
`email` varchar(32) NOT NULL COMMENT '邮箱',
`phone_number` varchar(15) NOT NULL COMMENT '手机号码',
`status` int(2) NOT NULL DEFAULT '1' COMMENT '状态,-1:逻辑删除,0:禁用,1:启用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`last_login_time` datetime DEFAULT NULL COMMENT '上次登录时间',
`last_update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `phone_number` (`phone_number`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='1.0-用户表';
================================================
FILE: demo-flyway/src/main/resources/db/migration/V1_1__ALTER.sql
================================================
ALTER TABLE t_user COMMENT = '用户 v1.1';
================================================
FILE: demo-flyway/src/test/java/com/xkcoding/AppTest.java
================================================
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);
}
}
================================================
FILE: demo-graylog/.gitignore
================================================
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
================================================
FILE: demo-graylog/README.md
================================================
# spring-boot-demo-graylog
> 此 demo 主要演示了 Spring Boot 项目如何接入 GrayLog 进行日志管理。
## 注意
作者在编写此 demo 时,`graylog` 采用 `docker-compose` 启动,其中 `graylog` 依赖的 `mongodb` 以及 `elasticsearch` 都同步启动,生产环境建议使用外部存储。
## 1. 环境准备
**编写 `graylog` 的 `docker-compose` 启动文件**
> 如果本地没有 `mongo:3` 和 `elasticsearch-oss:6.6.1` 的镜像,会比较耗时间
```yaml
version: '2'
services:
# MongoDB: https://hub.docker.com/_/mongo/
mongodb:
image: mongo:3
# Elasticsearch: https://www.elastic.co/guide/en/elasticsearch/reference/6.6/docker.html
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.1
environment:
- http.host=0.0.0.0
- transport.host=localhost
- network.host=0.0.0.0
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
mem_limit: 1g
# Graylog: https://hub.docker.com/r/graylog/graylog/
graylog:
image: graylog/graylog:3.0
environment:
# 加密盐值,不设置,graylog会启动失败
# 该字段最少需要16个字符
- GRAYLOG_PASSWORD_SECRET=somepasswordpepper
# 设置用户名
- GRAYLOG_ROOT_USERNAME=admin
# 设置密码,此为密码进过SHA256加密后的字符串
# 加密方式,执行 echo -n "Enter Password: " && head -1
4.0.0
spring-boot-demo-graylog
1.0.0-SNAPSHOT
jar
spring-boot-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
spring-boot-demo-graylog
org.springframework.boot
spring-boot-maven-plugin
```
## 3. application.yml
```yaml
spring:
application:
name: graylog
```
## 4. logback-spring.xml
```xml
${CONSOLE_LOG_PATTERN}
utf8
localhost
12201
508
true
true
true
true
false
false
true
${GRAY_LOG_SHORT_PATTERN}
${GRAY_LOG_FULL_PATTERN}
app_name:${APP_NAME}
os_arch:${os.arch}
os_name:${os.name}
os_version:${os.version}
```
## 5. 配置 graylog 控制台,接收日志来源
1. 登录 `graylog`,打开浏览器访问:http://localhost:9000
输入 `docker-compose.yml` 里配置的 `用户名/密码` 信息

2. 设置来源信息




## 6. 启动 Spring Boot 项目
启动成功后,返回graylog页面查看日志信息

## 参考
- graylog 官方下载地址:https://www.graylog.org/downloads#open-source
- graylog 官方docker镜像:https://hub.docker.com/r/graylog/graylog/
- graylog 镜像启动方式:http://docs.graylog.org/en/stable/pages/installation/docker.html
- graylog 启动参数配置:http://docs.graylog.org/en/stable/pages/configuration/server.conf.html
注意,启动参数需要加 `GRAYLOG_` 前缀
- 日志收集依赖:https://github.com/osiegmar/logback-gelf
================================================
FILE: demo-graylog/pom.xml
================================================
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
================================================
FILE: demo-graylog/src/main/java/com/xkcoding/graylog/SpringBootDemoGraylogApplication.java
================================================
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);
}
}
================================================
FILE: demo-graylog/src/main/resources/application.yml
================================================
spring:
application:
name: graylog
================================================
FILE: demo-graylog/src/main/resources/logback-spring.xml
================================================
${CONSOLE_LOG_PATTERN}
utf8
localhost
12201
508
true
true
true
true
false
false
true
${GRAY_LOG_SHORT_PATTERN}
${GRAY_LOG_FULL_PATTERN}
app_name:${APP_NAME}
os_arch:${os.arch}
os_name:${os.name}
os_version:${os.version}
================================================
FILE: demo-graylog/src/test/java/com/xkcoding/graylog/SpringBootDemoGraylogApplicationTests.java
================================================
package com.xkcoding.graylog;
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 SpringBootDemoGraylogApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-helloworld/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-helloworld/README.md
================================================
# 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
```
================================================
FILE: demo-helloworld/pom.xml
================================================
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
================================================
FILE: demo-helloworld/src/main/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplication.java
================================================
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);
}
}
================================================
FILE: demo-helloworld/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
================================================
FILE: demo-helloworld/src/test/java/com/xkcoding/helloworld/SpringBootDemoHelloworldApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-https/.gitignore
================================================
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
================================================
FILE: demo-https/README.md
================================================
# spring-boot-demo-https
> 此 demo 主要演示了 Spring Boot 如何集成 https
## 1. 生成证书
首先使用 jdk 自带的 keytool 命令生成证书复制到项目的 `resources` 目录下(生成的证书一般在用户目录下 C:\Users\Administrator\server.keystore)
> 自己生成的证书浏览器会有危险提示,去ssl网站上使用金钱申请则不会

## 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)
================================================
FILE: demo-https/pom.xml
================================================
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
================================================
FILE: demo-https/src/main/java/com/xkcoding/https/SpringBootDemoHttpsApplication.java
================================================
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);
}
}
================================================
FILE: demo-https/src/main/java/com/xkcoding/https/config/HttpsConfig.java
================================================
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;
}
}
================================================
FILE: demo-https/src/main/resources/application.yml
================================================
server:
ssl:
# 证书路径
key-store: classpath:server.keystore
key-alias: tomcat
enabled: true
key-store-type: JKS
#与申请时输入一致
key-store-password: 123456
# 浏览器默认端口 和 80 类似
port: 443
================================================
FILE: demo-https/src/main/resources/static/index.html
================================================
spring boot demo https
spring boot demo https
================================================
FILE: demo-https/src/test/java/com/xkcoding/https/SpringBootDemoHttpsApplicationTests.java
================================================
package com.xkcoding.https;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootDemoHttpsApplicationTests {
@Test
void contextLoads() {
}
}
================================================
FILE: demo-ldap/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-ldap/README.md
================================================
# 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/
================================================
FILE: demo-ldap/pom.xml
================================================
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
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/LdapDemoApplication.java
================================================
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);
}
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/api/Result.java
================================================
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);
}
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/api/ResultCode.java
================================================
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;
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/entity/Person.java
================================================
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;
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/exception/ServiceException.java
================================================
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);
}
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/repository/PersonRepository.java
================================================
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);
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/request/LoginRequest.java
================================================
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;
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/service/PersonService.java
================================================
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);
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/service/impl/PersonServiceImpl.java
================================================
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());
}
}
================================================
FILE: demo-ldap/src/main/java/com/xkcoding/ldap/util/LdapUtils.java
================================================
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();
}
}
================================================
FILE: demo-ldap/src/main/resources/application.yml
================================================
spring:
ldap:
urls: ldap://localhost:389
base: dc=example,dc=org
username: cn=admin,dc=example,dc=org
password: admin
================================================
FILE: demo-ldap/src/test/java/com/xkcoding/ldap/LdapDemoApplicationTests.java
================================================
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);
}
}
================================================
FILE: demo-log-aop/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-log-aop/README.md
================================================
# 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);
}
}
```
================================================
FILE: demo-log-aop/pom.xml
================================================
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
================================================
FILE: demo-log-aop/src/main/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplication.java
================================================
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);
}
}
================================================
FILE: demo-log-aop/src/main/java/com/xkcoding/log/aop/aspectj/AopLog.java
================================================
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;
}
}
================================================
FILE: demo-log-aop/src/main/java/com/xkcoding/log/aop/controller/TestController.java
================================================
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);
}
}
================================================
FILE: demo-log-aop/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
================================================
FILE: demo-log-aop/src/main/resources/logback-spring.xml
================================================
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
================================================
FILE: demo-log-aop/src/test/java/com/xkcoding/log/aop/SpringBootDemoLogAopApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-logback/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-logback/README.md
================================================
# 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
```
================================================
FILE: demo-logback/pom.xml
================================================
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
================================================
FILE: demo-logback/src/main/java/com/xkcoding/logback/SpringBootDemoLogbackApplication.java
================================================
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);
}
}
}
================================================
FILE: demo-logback/src/main/resources/application.yml
================================================
server:
port: 8080
servlet:
context-path: /demo
================================================
FILE: demo-logback/src/main/resources/logback-spring.xml
================================================
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
================================================
FILE: demo-logback/src/test/java/com/xkcoding/logback/SpringBootDemoLogbackApplicationTests.java
================================================
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() {
}
}
================================================
FILE: demo-mongodb/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-mongodb/README.md
================================================
# 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/
================================================
FILE: demo-mongodb/pom.xml
================================================
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
================================================
FILE: demo-mongodb/src/main/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplication.java
================================================
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);
}
}
================================================
FILE: demo-mongodb/src/main/java/com/xkcoding/mongodb/model/Article.java
================================================
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;
}
================================================
FILE: demo-mongodb/src/main/java/com/xkcoding/mongodb/repository/ArticleRepository.java
================================================
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);
}
================================================
FILE: demo-mongodb/src/main/resources/application.yml
================================================
spring:
data:
mongodb:
host: localhost
port: 27017
database: article_db
logging:
level:
org.springframework.data.mongodb.core: debug
================================================
FILE: demo-mongodb/src/test/java/com/xkcoding/mongodb/SpringBootDemoMongodbApplicationTests.java
================================================
package com.xkcoding.mongodb;
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 SpringBootDemoMongodbApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-mongodb/src/test/java/com/xkcoding/mongodb/repository/ArticleRepositoryTest.java
================================================
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));
}
}
================================================
FILE: demo-mq-kafka/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-mq-kafka/README.md
================================================
# 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/
================================================
FILE: demo-mq-kafka/pom.xml
================================================
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
================================================
FILE: demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplication.java
================================================
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);
}
}
================================================
FILE: demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/config/KafkaConfig.java
================================================
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;
}
}
================================================
FILE: demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/constants/KafkaConsts.java
================================================
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";
}
================================================
FILE: demo-mq-kafka/src/main/java/com/xkcoding/mq/kafka/handler/MessageHandler.java
================================================
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();
}
}
}
================================================
FILE: demo-mq-kafka/src/main/resources/application.yml
================================================
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
================================================
FILE: demo-mq-kafka/src/test/java/com/xkcoding/mq/kafka/SpringBootDemoMqKafkaApplicationTests.java
================================================
package com.xkcoding.mq.kafka;
import com.xkcoding.mq.kafka.constants.KafkaConsts;
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.kafka.core.KafkaTemplate;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootDemoMqKafkaApplicationTests {
@Autowired
private KafkaTemplate kafkaTemplate;
/**
* 测试发送消息
*/
@Test
public void testSend() {
kafkaTemplate.send(KafkaConsts.TOPIC_TEST, "hello,kafka...");
}
}
================================================
FILE: demo-mq-rabbitmq/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-mq-rabbitmq/README.md
================================================
# 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;
});
}
}
```
## 运行效果
### 直接模式

### 分列模式

### 主题模式
#### RoutingKey:`queue.#`

#### RoutingKey:`*.queue`

#### RoutingKey:`3.queue`

### 延迟队列

## 参考
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
================================================
FILE: demo-mq-rabbitmq/pom.xml
================================================
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
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplication.java
================================================
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);
}
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/config/RabbitMqConfig.java
================================================
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();
}
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/constants/RabbitConsts.java
================================================
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";
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DelayQueueHandler.java
================================================
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();
}
}
}
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/DirectQueueOneHandler.java
================================================
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();
}
}
}
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueThreeHandler.java
================================================
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();
}
}
}
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/handler/QueueTwoHandler.java
================================================
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();
}
}
}
}
================================================
FILE: demo-mq-rabbitmq/src/main/java/com/xkcoding/mq/rabbitmq/message/MessageStruct.java
================================================
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;
}
================================================
FILE: demo-mq-rabbitmq/src/main/resources/application.yml
================================================
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
================================================
FILE: demo-mq-rabbitmq/src/test/java/com/xkcoding/mq/rabbitmq/SpringBootDemoMqRabbitmqApplicationTests.java
================================================
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;
});
}
}
================================================
FILE: demo-mq-rocketmq/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-mq-rocketmq/README.md
================================================
================================================
FILE: demo-mq-rocketmq/pom.xml
================================================
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
================================================
FILE: demo-mq-rocketmq/src/main/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplication.java
================================================
package com.xkcoding.mq.rocketmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootDemoMqRocketmqApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoMqRocketmqApplication.class, args);
}
}
================================================
FILE: demo-mq-rocketmq/src/main/resources/application.properties
================================================
================================================
FILE: demo-mq-rocketmq/src/test/java/com/xkcoding/mq/rocketmq/SpringBootDemoMqRocketmqApplicationTests.java
================================================
package com.xkcoding.mq.rocketmq;
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 SpringBootDemoMqRocketmqApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-multi-datasource-jpa/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-multi-datasource-jpa/README.md
================================================
# 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
================================================
FILE: demo-multi-datasource-jpa/pom.xml
================================================
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
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplication.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryDataSourceConfig.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/PrimaryJpaConfig.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondDataSourceConfig.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SecondJpaConfig.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/config/SnowflakeConfig.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/primary/PrimaryMultiTable.java
================================================
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;
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/entity/second/SecondMultiTable.java
================================================
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;
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/primary/PrimaryMultiTableRepository.java
================================================
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 {
}
================================================
FILE: demo-multi-datasource-jpa/src/main/java/com/xkcoding/multi/datasource/jpa/repository/second/SecondMultiTableRepository.java
================================================
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 {
}
================================================
FILE: demo-multi-datasource-jpa/src/main/resources/application.yml
================================================
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
================================================
FILE: demo-multi-datasource-jpa/src/test/java/com/xkcoding/multi/datasource/jpa/SpringBootDemoMultiDatasourceJpaApplicationTests.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);
}
}
================================================
FILE: demo-multi-datasource-mybatis/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-multi-datasource-mybatis/README.md
================================================
# 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** 操作

**从**库 **建议** 只执行 **SELECT** 操作

> 生产环境需要搭建 **主从复制**
## 参考
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
================================================
FILE: demo-multi-datasource-mybatis/pom.xml
================================================
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
================================================
FILE: demo-multi-datasource-mybatis/sql/db.sql
================================================
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;
================================================
FILE: demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplication.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/mapper/UserMapper.java
================================================
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 {
}
================================================
FILE: demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/model/User.java
================================================
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;
}
================================================
FILE: demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/UserService.java
================================================
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);
}
================================================
FILE: demo-multi-datasource-mybatis/src/main/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImpl.java
================================================
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);
}
}
================================================
FILE: demo-multi-datasource-mybatis/src/main/resources/application.yml
================================================
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
================================================
FILE: demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/SpringBootDemoMultiDatasourceMybatisApplicationTests.java
================================================
package com.xkcoding.multi.datasource.mybatis;
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 SpringBootDemoMultiDatasourceMybatisApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-multi-datasource-mybatis/src/test/java/com/xkcoding/multi/datasource/mybatis/service/impl/UserServiceImplTest.java
================================================
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));
}
}
================================================
FILE: demo-neo4j/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-neo4j/README.md
================================================
# 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里所有节点和关系

## 参考
- 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/
================================================
FILE: demo-neo4j/pom.xml
================================================
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
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplication.java
================================================
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);
}
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/config/CustomIdStrategy.java
================================================
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();
}
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/constants/NeoConsts.java
================================================
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";
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Class.java
================================================
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;
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Lesson.java
================================================
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;
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Student.java
================================================
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;
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/model/Teacher.java
================================================
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;
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/ClassmateInfoGroupByLesson.java
================================================
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;
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/payload/TeacherStudent.java
================================================
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;
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/ClassRepository.java
================================================
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);
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/LessonRepository.java
================================================
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 {
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/StudentRepository.java
================================================
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();
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/repository/TeacherRepository.java
================================================
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 {
}
================================================
FILE: demo-neo4j/src/main/java/com/xkcoding/neo4j/service/NeoService.java
================================================
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;
}
}
================================================
FILE: demo-neo4j/src/main/resources/application.yml
================================================
spring:
data:
neo4j:
uri: bolt://localhost
username: neo4j
password: admin
open-in-view: false
================================================
FILE: demo-neo4j/src/test/java/com/xkcoding/neo4j/Neo4jTest.java
================================================
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()))));
}
}
================================================
FILE: demo-neo4j/src/test/java/com/xkcoding/neo4j/SpringBootDemoNeo4jApplicationTests.java
================================================
package com.xkcoding.neo4j;
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 SpringBootDemoNeo4jApplicationTests {
@Test
public void contextLoads() {
}
}
================================================
FILE: demo-oauth/.gitignore
================================================
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
================================================
FILE: demo-oauth/README.md
================================================
# spring-boot-demo-oauth
================================================
FILE: demo-oauth/oauth-authorization-server/README.adoc
================================================
= spring-boot-demo-oauth-authorization-server
Doc Writer
v1.0, 2019-01-07
:toc:
spring boot oauth2 授权服务器,
- 授权码模式、密码模式、刷新令牌
- 自定义 UserDetailService
- 自定义 ClientDetailService
- jwt 非对称加密
- 自定义登录授权页面
> SQL 语句
>
> - DDL: `src/test/resources/schema.sql`
> - DML: `src/test/resources/import.sql`
测试用例使用 h2 数据库,测试数据如下:
.测试客户端
|===
|客户端 id |客户端密钥 |资源服务器名称 |授权类型 | scopes| 回调地址
|oauth2
|oauth2
|oauth2
|authorization_code,password,refresh_token
|READ,WRITE
|http://example.com
|test
|oauth2
|oauth2
|authorization_code,password,refresh_token
|READ
|http://example.com
|error
|oauth2
|test
|authorization_code,password,refresh_token
|READ
|http://example.com
|===
.测试用户
|===
|用户名 |密码 |角色
|admin
|123456
|ROLE_ADMIN
|test
|123456
|ROLE_TEST
|===
== 授权码模式
> 测试用例:`com.xkcoding.oauth.oauth.AuthorizationCodeGrantTests`
=== 获取授权码
- 请求地址: http://localhost:8080/oauth/authorize?response_type=code&client_id=oauth2&redirect_uri=http://example.com&scope=READ
- 用户名:admin
- 密码:123456
image::image/Login.png[login]
=== 确认授权
登录成功以后,进入确认授权页面。已经确认过的用户,不会再次要求确认。
image::image/Confirm.png[confirm]
确认授权后,获取授权码
image::image/Code.png[code]
=== 请求 token
使用以下代码可以直接请求 token
[shell]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=GgX6QD' \
--data-urlencode 'redirect_uri=http://example.com' \
--data-urlencode 'client_id=oauth2' \
--data-urlencode 'scope=READ WRITE'
----
得到 token
[token]
----
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjAyMDhiNTUtYTJjYS00NjI4LTg5YjEtNzI5MzY4MzAxOWNhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.RqJpsin6bMnwI57cGpODTplLeW_gtNWHo_l4SimyRLsnxpCWm5oY1EOb4qVHpXvCbhNsUj69D462P7le13OOmexysZIQhaoGZ_CbIlEp63XsCnr5nSKeX3dgQlyTUDjOUL0WUtY2lKqLCGMeX_rpVhfmSh3b7MC0Ntxq5ao-943QMXGRIeRvJgSkvfY2HBN6-zx1H6rE0wxnUfBC1M08kUkFYlSmsFchiz-E_oTzJvE2D8lA9g-eEFU6cZ_els4Q77Vvc_O6SXUZ7o65vFyLyUjLvh9QF1825SGIUUdXTUYSZjnSAXChhRIAT5pLRHK-gthIzpOaWrgj6ebUoG02Eg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJhdGkiOiJmMDIwOGI1NS1hMmNhLTQ2MjgtODliMS03MjkzNjgzMDE5Y2EiLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMGViNTU2MTQtYjgxYS00MTFmLTg1MTAtZThkMjZmODJmMjJhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.CBGcjirkf-3187SgbZr0ikauiCS8U9YLaoR4sNlRQjd-gaIeF5PChnIs_yAmG_VpqPFlPRdSl8DA05S2QnFpT3TkRjyP-LPDZgsVAPfczMAdVywU1zOKYZeq-gM6p9bmGEabbZoBlIxOImsjeyFSCui6UtRTZjNlj3AhGIzvs52T8bDqC796iHPDZvJ97MMgsEiRyu-mxDm1o1LMuBX9RHCx9rAkBVf52q36bqWMcYAlDOu1wYjpmhalSLZyWcmraQvClEitXGJI4eTFapTnuXQuWFIL-973V_5Shw98-bk65zZQOEheazHrUf-n4h-sYT4akehnYSVxX2UIg9XsCw",
"expires_in": 5999,
"scope": "READ",
"jti": "f0208b55-a2ca-4628-89b1-7293683019ca"
}
----
== 密码模式
> 测试用例:`com.xkcoding.oauth.oauth.ResourceOwnerPasswordGrantTests`
`test` 用户进行授权
[source]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'password=123456' \
--data-urlencode 'username=test' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=READ WRITE'
----
== 刷新令牌
携带 `refresh_token` 去请求
[source]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJhdGkiOiJmMDIwOGI1NS1hMmNhLTQ2MjgtODliMS03MjkzNjgzMDE5Y2EiLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMGViNTU2MTQtYjgxYS00MTFmLTg1MTAtZThkMjZmODJmMjJhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.CBGcjirkf-3187SgbZr0ikauiCS8U9YLaoR4sNlRQjd-gaIeF5PChnIs_yAmG_VpqPFlPRdSl8DA05S2QnFpT3TkRjyP-LPDZgsVAPfczMAdVywU1zOKYZeq-gM6p9bmGEabbZoBlIxOImsjeyFSCui6UtRTZjNlj3AhGIzvs52T8bDqC796iHPDZvJ97MMgsEiRyu-mxDm1o1LMuBX9RHCx9rAkBVf52q36bqWMcYAlDOu1wYjpmhalSLZyWcmraQvClEitXGJI4eTFapTnuXQuWFIL-973V_5Shw98-bk65zZQOEheazHrUf-n4h-sYT4akehnYSVxX2UIg9XsCw'
----
== 解析令牌
携带令牌解析
[source]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/check_token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'token='
----
解析结果
[source]
----
{
"aud": [
"oauth2"
],
"user_name": "admin",
"scope": [
"READ",
"WRITE"
],
"active": true,
"exp": 1578389936,
"authorities": [
"ROLE_ADMIN"
],
"jti": "fe59fce9-6764-435e-8fa7-7320e11af811",
"client_id": "oauth2"
}
----
== 退出登录
授权码模式登陆是在授权服务器上登录的,所以退出也要在授权服务器上退出。
携带回调地址进行退出,退出完成后跳转到回调地址:
image::image/Logout.png[logout]
退出以后自动跳转到回调地址(要加 `http` 或 `https`)
== 获取公钥
通过访问 '/oauth/token_key' 获取 JWT 公钥
[source]
----
curl --location --request GET 'http://127.0.0.1:8080/oauth/token_key' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg=='
----
获取后
[source]
----
{
"alg": "SHA256withRSA",
"value": "-----BEGIN PUBLIC KEY-----\n......\n-----END PUBLIC KEY-----"
}
----
== 核心配置
=== 授权服务器配置
[Oauth2AuthorizationServerConfig]
----
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
// 自定义用户
.userDetailsService(sysUserService)
// 内存存储
.tokenStore(tokenStore)
// jwt 令牌转换
.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()");
}
----
=== 安全配置
[WebSecurityConfig]
----
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 开启表单登录,授权码模式的时候进行登录
.formLogin()
// 路径等
.loginPage("/oauth/login")
.loginProcessingUrl("/authorization/form")
// 失败以后携带错误信息进行再次跳转登录页面
.failureHandler(clientLoginFailureHandler)
.and()
// 退出登录相关
.logout()
.logoutUrl("/oauth/logout")
.logoutSuccessHandler(clientLogoutSuccessHandler)
.and()
// 授权服务器安全配置
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest()
.authenticated();
}
----
== 参考
- https://echocow.cn/articles/2019/07/14/1563096109754.html[Spring Security Oauth2 从零到一完整实践(三)授权服务器 ]
================================================
FILE: demo-oauth/oauth-authorization-server/pom.xml
================================================
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
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
================================================
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);
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java
================================================
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"));
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java
================================================
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"));
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java
================================================
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()");
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java
================================================
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();
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java
================================================
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();
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java
================================================
/**
* 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;
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java
================================================
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;
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java
================================================
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;
}
}
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java
================================================
/**
* 控制器。除了业务逻辑的以外,提供两个控制器来帮助完成自定义:
* {@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;
================================================
FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java
================================================
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