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 ================================================

Spring Boot Demo

Travis-CI Codacy author JDK Spring Boot LICENSE

star star star

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 [![Stargazers over time](https://starchart.cc/xkcoding/spring-boot-demo.svg)](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 - jetbrains**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 ================================================

Spring Boot Demo

Travis-CI Codacy author JDK Spring Boot LICENSE

star star star

中文 | 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 之前,有些是需要事先初始化数据库数据的,亲们别忘记了哦~`** ## 项目趋势 [![Stargazers over time](https://starchart.cc/xkcoding/spring-boot-demo.svg)](https://starchart.cc/xkcoding/spring-boot-demo) ## 其他 ### 团队纳新 组内招人啦,HC 巨多,Base 杭州,感兴趣的小伙伴,查看 [岗位详情](./jd.md) ### 开源推荐 ![11628591293_.pic_hd](https://static.aliyun.xkcoding.com/2021/08/14/11628591293pichd.jpg?x-oss-process=style/tag_compress) - `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**感谢 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 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. 演示 ## 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}} 查询

2019 © xkcoding

生成配置

取消 生成代码
================================================ 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. 测试 启动项目,可以看到控制台读取到数据库已配置的数据源信息 ![image-20190905164824155](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062351.png) 通过 PostMan 等工具测试 - 默认数据源查询 ![image-20190905165240373](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062353.png) - 根据数据源id为1的数据源查询 ![image-20190905165323097](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062354.png) - 根据数据源id为2的数据源查询 ![image-20190905165350355](http://static.xkcoding.com/spring-boot-demo/dynamic-datasource/062355.png) - 可以通过测试数据源的`增加/删除`,再去查询对应数据源的数据 > 删除数据源: > > - DELETE http://localhost:8080/config/{id} > > 新增数据源: > > - POST http://localhost:8080/config > > - 参数: > > ```json > { > "host": "数据库IP", > "port": 3306, > "username": "用户名", > "password": "密码", > "database": "数据库" > } > ``` ## 4. 优化 如上测试,我们只需要通过在 header 里传递数据源的参数,即可做到动态切换数据源,怎么做到的呢? 答案就是 `AOP` ```java /** *

* 数据源选择器切面 *

* * @author yangkai.shen * @date Created in 2019-09-04 16:52 */ @Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class DatasourceSelectorAspect { @Pointcut("execution(public * com.xkcoding.dynamic.datasource.controller.*.*(..))") public void datasourcePointcut() { } /** * 前置操作,拦截具体请求,获取header里的数据源id,设置线程变量里,用于后续切换数据源 */ @Before("datasourcePointcut()") public void doBefore(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); // 排除不可切换数据源的方法 DefaultDatasource annotation = method.getAnnotation(DefaultDatasource.class); if (null != annotation) { DatasourceConfigContextHolder.setDefaultDatasource(); } else { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = attributes.getRequest(); String configIdInHeader = request.getHeader("Datasource-Config-Id"); if (StringUtils.hasText(configIdInHeader)) { long configId = Long.parseLong(configIdInHeader); DatasourceConfigContextHolder.setCurrentDatasourceConfig(configId); } else { DatasourceConfigContextHolder.setDefaultDatasource(); } } } /** * 后置操作,设置回默认的数据源id */ @AfterReturning("datasourcePointcut()") public void doAfter() { DatasourceConfigContextHolder.setDefaultDatasource(); } } ``` 此时需要考虑,我们是否每个方法都允许用户去切换数据源呢?答案肯定是不行的,所以我们定义了一个注解去标识,当前方法仅可以使用默认数据源。 ```java /** *

* 用户标识仅可以使用默认数据源 *

* * @author yangkai.shen * @date Created in 2019-09-04 17:37 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DefaultDatasource { } ``` 完结,撒花✿✿ヽ(°▽°)ノ✿ ================================================ 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` 表 image-20200305105632047 查看下 `flyway-schema-history` 表 image-20200305110147176 ### 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` 表的注释已经更新 image-20200305105958181 查看下 `flyway-schema-history` 表 image-20200305110057768 ## 参考 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` 里配置的 `用户名/密码` 信息 ![登录graylog](http://static.xkcoding.com/spring-boot-demo/graylog/063124.jpg) 2. 设置来源信息 ![设置Inputs](http://static.xkcoding.com/spring-boot-demo/graylog/063125.jpg) ![image-20190423164748993](http://static.xkcoding.com/spring-boot-demo/graylog/063121-1.jpg) ![image-20190423164932488](http://static.xkcoding.com/spring-boot-demo/graylog/063121.jpg) ![image-20190423165120586](http://static.xkcoding.com/spring-boot-demo/graylog/063122.jpg) ## 6. 启动 Spring Boot 项目 启动成功后,返回graylog页面查看日志信息 ![image-20190423165936711](http://static.xkcoding.com/spring-boot-demo/graylog/063123.jpg) ## 参考 - 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网站上使用金钱申请则不会 ![ssl 命令截图](ssl.png) ## 2. 添加配置 1. 在配置文件配置生成的证书 ```yaml server: ssl: # 证书路径 key-store: classpath:server.keystore key-alias: tomcat enabled: true key-store-type: JKS #与申请时输入一致 key-store-password: 123456 # 浏览器默认端口 和 80 类似 port: 443 ``` 2. 配置 Tomcat ```java /** *

* HTTPS 配置类 *

* * @author yangkai.shen * @date Created in 2020-01-19 10:31 */ @Configuration public class HttpsConfig { /** * 配置 http(80) -> 强制跳转到 https(443) */ @Bean public Connector connector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); connector.setPort(80); connector.setSecure(false); connector.setRedirectPort(443); return connector; } @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector) { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(connector); return tomcat; } } ``` ## 3. 测试 启动项目,浏览器访问 http://localhost 将自动跳转到 https://localhost ## 4. 参考 - `keytool`命令参考 ```bash $ keytool --help 密钥和证书管理工具 命令: -certreq 生成证书请求 -changealias 更改条目的别名 -delete 删除条目 -exportcert 导出证书 -genkeypair 生成密钥对 -genseckey 生成密钥 -gencert 根据证书请求生成证书 -importcert 导入证书或证书链 -importpass 导入口令 -importkeystore 从其他密钥库导入一个或所有条目 -keypasswd 更改条目的密钥口令 -list 列出密钥库中的条目 -printcert 打印证书内容 -printcertreq 打印证书请求的内容 -printcrl 打印 CRL 文件的内容 -storepasswd 更改密钥库的存储口令 使用 "keytool -command_name -help" 获取 command_name 的用法 ``` - [Java Keytool工具简介](https://blog.csdn.net/liumiaocn/article/details/61921014) ================================================ 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; }); } } ``` ## 运行效果 ### 直接模式 ![image-20190107103229408](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063315-1.jpg) ### 分列模式 ![image-20190107103258291](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063315.jpg) ### 主题模式 #### RoutingKey:`queue.#` ![image-20190107103358744](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063316.jpg) #### RoutingKey:`*.queue` ![image-20190107103429430](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063312.jpg) #### RoutingKey:`3.queue` ![image-20190107103451240](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063313.jpg) ### 延迟队列 ![image-20190107103509943](http://static.xkcoding.com/spring-boot-demo/mq/rabbitmq/063314.jpg) ## 参考 1. SpringQP 官方文档:https://docs.spring.io/spring-amqp/docs/2.1.0.RELEASE/reference/html/ 2. RabbitMQ 官网:http://www.rabbitmq.com/ 3. RabbitMQ延迟队列:https://www.cnblogs.com/vipstone/p/9967649.html ================================================ 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** 操作 ![image-20190121153211509](http://static.xkcoding.com/spring-boot-demo/multi-datasource/mybatis/063506.jpg) **从**库 **建议** 只执行 **SELECT** 操作 ![image-20190121152825859](http://static.xkcoding.com/spring-boot-demo/multi-datasource/mybatis/063505.jpg) > 生产环境需要搭建 **主从复制** ## 参考 1. Mybatis-Plus 多数据源文档:https://mybatis.plus/guide/dynamic-datasource.html 2. Mybatis-Plus 多数据源集成官方 demo:https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter/tree/master/samples ================================================ 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里所有节点和关系 ![image-20181225150513101](http://static.xkcoding.com/spring-boot-demo/neo4j/063605.jpg) ## 参考 - spring-data-neo4j 官方文档:https://docs.spring.io/spring-data/neo4j/docs/5.1.2.RELEASE/reference/html/ - neo4j 官方文档:https://neo4j.com/docs/getting-started/3.5/ ================================================ 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 stringToSet(String s) { return Arrays.stream(s.split(",")).collect(Collectors.toSet()); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java ================================================ package com.xkcoding.oauth.entity; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import javax.persistence.*; import java.util.Set; /** * 这里完全可以只用一个字段代替的 * 但是想了想还是模拟实际的情况来把 * 角色信息. * * @author EchoCow * @date 2020-01-06 12:44 */ @Data @Table @Entity @EqualsAndHashCode(exclude = {"users"}) @ToString(exclude = "users") public class SysRole { /** * 主键. */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 角色名称,按照 spring security 规范 * 需要以 ROLE_ 开头. */ private String name; /** * 角色描述. */ private String description; /** * 当前角色所有用户. */ @ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER) private Set users; } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java ================================================ package com.xkcoding.oauth.entity; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; import java.util.Set; /** * 用户实体. * 避免实体类耦合,所以不去实现 {@link UserDetails} 接口 * 因为有且只有登录加载用户的时候才会需要这个接口 * 我们就手动构建一个 {@link User} 的默认实现就可以了 * 实现接口的方式可以参考 {@link SysClientDetails} * * @author EchoCow * @date 2020-01-06 12:41 */ @Data @Table @Entity @EqualsAndHashCode(exclude = "roles") @ToString(exclude = "roles") public class SysUser { /** * 主键. */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 用户名. */ private String username; /** * 密码. */ private String password; /** * 当前用户所有角色. */ @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Set roles; } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java ================================================ package com.xkcoding.oauth.repostiory; import com.xkcoding.oauth.entity.SysClientDetails; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import java.util.Optional; /** * 客户端信息. * * @author EchoCow * @date 2020-01-06 13:09 */ public interface SysClientDetailsRepository extends JpaRepository { /** * 通过 clientId 查找客户端信息. * * @param clientId clientId * @return 结果 */ Optional findFirstByClientId(String clientId); /** * 根据客户端 id 删除客户端 * * @param clientId 客户端id */ @Modifying void deleteByClientId(String clientId); } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java ================================================ package com.xkcoding.oauth.repostiory; import com.xkcoding.oauth.entity.SysUser; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; /** * 用户信息仓库. * * @author EchoCow * @date 2020-01-06 13:08 */ public interface SysUserRepository extends JpaRepository { /** * 通过用户名查找用户. * * @param username 用户名 * @return 结果 */ Optional findFirstByUsername(String username); } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java ================================================ package com.xkcoding.oauth.service; import com.xkcoding.oauth.entity.SysClientDetails; import org.springframework.security.oauth2.provider.ClientAlreadyExistsException; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationService; import org.springframework.security.oauth2.provider.NoSuchClientException; import java.util.List; /** * 声明自己的实现. * 参见 {@link ClientRegistrationService} * * @author EchoCow * @date 2020-01-06 13:39 */ public interface SysClientDetailsService extends ClientDetailsService { /** * 通过客户端 id 查询 * * @param clientId 客户端 id * @return 结果 */ SysClientDetails findByClientId(String clientId); /** * 添加客户端信息. * * @param clientDetails 客户端信息 * @throws ClientAlreadyExistsException 客户端已存在 */ void addClientDetails(SysClientDetails clientDetails) throws ClientAlreadyExistsException; /** * 更新客户端信息,不包括 clientSecret. * * @param clientDetails 客户端信息 * @throws NoSuchClientException 找不到客户端异常 */ void updateClientDetails(SysClientDetails clientDetails) throws NoSuchClientException; /** * 更新客户端密钥. * * @param clientId 客户端 id * @param clientSecret 客户端密钥 * @throws NoSuchClientException 找不到客户端异常 */ void updateClientSecret(String clientId, String clientSecret) throws NoSuchClientException; /** * 删除客户端信息. * * @param clientId 客户端 id * @throws NoSuchClientException 找不到客户端异常 */ void removeClientDetails(String clientId) throws NoSuchClientException; /** * 查询所有 * * @return 结果 */ List findAll(); } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java ================================================ package com.xkcoding.oauth.service; import com.xkcoding.oauth.entity.SysUser; import org.springframework.security.core.userdetails.UserDetailsService; import java.util.List; /** * . * * @author EchoCow * @date 2020-01-06 15:44 */ public interface SysUserService extends UserDetailsService { /** * 查询所有用户 * * @return 用户 */ List findAll(); /** * 通过 id 查询用户 * * @param id id * @return 用户 */ SysUser findById(Long id); /** * 创建用户 * * @param sysUser 用户 */ void createUser(SysUser sysUser); /** * 更新用户 * * @param sysUser 用户 */ void updateUser(SysUser sysUser); /** * 更新用户 密码 * * @param id 用户 id * @param password 用户密码 */ void updatePassword(Long id, String password); /** * 删除用户. * * @param id id */ void deleteUser(Long id); } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java ================================================ package com.xkcoding.oauth.service.impl; import com.xkcoding.oauth.entity.SysClientDetails; import com.xkcoding.oauth.repostiory.SysClientDetailsRepository; import com.xkcoding.oauth.service.SysClientDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.ClientAlreadyExistsException; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.stereotype.Service; import java.util.List; /** * 客户端 相关操作. * * @author EchoCow * @date 2020-01-06 13:37 */ @Service @RequiredArgsConstructor public class SysClientDetailsServiceImpl implements SysClientDetailsService { private final SysClientDetailsRepository sysClientDetailsRepository; private final PasswordEncoder passwordEncoder; @Override public ClientDetails loadClientByClientId(String id) throws ClientRegistrationException { return sysClientDetailsRepository.findFirstByClientId(id).orElseThrow(() -> new ClientRegistrationException("Loading client exception.")); } @Override public SysClientDetails findByClientId(String clientId) { return sysClientDetailsRepository.findFirstByClientId(clientId).orElseThrow(() -> new ClientRegistrationException("Loading client exception.")); } @Override public void addClientDetails(SysClientDetails clientDetails) throws ClientAlreadyExistsException { clientDetails.setId(null); if (sysClientDetailsRepository.findFirstByClientId(clientDetails.getClientId()).isPresent()) { throw new ClientAlreadyExistsException(String.format("Client id %s already exist.", clientDetails.getClientId())); } sysClientDetailsRepository.save(clientDetails); } @Override public void updateClientDetails(SysClientDetails clientDetails) throws NoSuchClientException { SysClientDetails exist = sysClientDetailsRepository.findFirstByClientId(clientDetails.getClientId()).orElseThrow(() -> new NoSuchClientException("No such client!")); clientDetails.setClientSecret(exist.getClientSecret()); sysClientDetailsRepository.save(clientDetails); } @Override public void updateClientSecret(String clientId, String clientSecret) throws NoSuchClientException { SysClientDetails exist = sysClientDetailsRepository.findFirstByClientId(clientId).orElseThrow(() -> new NoSuchClientException("No such client!")); exist.setClientSecret(passwordEncoder.encode(clientSecret)); sysClientDetailsRepository.save(exist); } @Override public void removeClientDetails(String clientId) throws NoSuchClientException { sysClientDetailsRepository.deleteByClientId(clientId); } @Override public List findAll() { return sysClientDetailsRepository.findAll(); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java ================================================ package com.xkcoding.oauth.service.impl; import com.xkcoding.oauth.entity.SysUser; import com.xkcoding.oauth.repostiory.SysUserRepository; import com.xkcoding.oauth.service.SysUserService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; /** * 用户相关操作. * * @author EchoCow * @date 2020-01-06 15:06 */ @Service @RequiredArgsConstructor public class SysUserServiceImpl implements SysUserService { private final SysUserRepository sysUserRepository; private final PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserRepository.findFirstByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found!")); List roles = sysUser.getRoles().stream().map(sysRole -> new SimpleGrantedAuthority(sysRole.getName())).collect(Collectors.toList()); // 在这里手动构建 UserDetails 的默认实现 return new User(sysUser.getUsername(), sysUser.getPassword(), roles); } @Override public List findAll() { return sysUserRepository.findAll(); } @Override public SysUser findById(Long id) { return sysUserRepository.findById(id).orElseThrow(() -> new RuntimeException("找不到用户")); } @Override public void createUser(SysUser sysUser) { sysUser.setId(null); sysUserRepository.save(sysUser); } @Override public void updateUser(SysUser sysUser) { sysUser.setPassword(null); sysUserRepository.save(sysUser); } @Override public void updatePassword(Long id, String password) { SysUser exist = findById(id); exist.setPassword(passwordEncoder.encode(password)); sysUserRepository.save(exist); } @Override public void deleteUser(Long id) { sysUserRepository.deleteById(id); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java ================================================ /** * service 层,继承并实现 spring 接口. * * @author EchoCow * @date 2020-01-07 9:16 */ package com.xkcoding.oauth.service; ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/application.yml ================================================ server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/oauth?allowPublicKeyRetrieval=true username: root password: 123456 hikari: data-source-properties: useSSL: false serverTimezone: GMT+8 useUnicode: true characterEncoding: utf8 jpa: hibernate: ddl-auto: update show-sql: true logging: level: org.springframework.security: debug ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/public.txt ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkF9SyMHeGAsLMwbPsKj/ xpEtS0iCe8vTSBnIGBDZKmB3ma20Ry0Uzn3m+f40RwCXlxnUcvTw7ipoz0tMQERQ b3X4DkYCJXPK6pAD+R9/J5odEwrO2eysByWfcbMjsZw2u5pH5hleMS0YqkrGQOxJ pzlEcKxMePU5KYTbKUJkhOYPY+gQr61g6lF97WggSPtuQn1srT+Ptvfw6yRC4bdI 0zV5emfXjmoLUwaQTRoGYhOFrm97vpoKiltSNIDFW01J1Lr+l77ddDFC6cdiAC0H 5/eENWBBBTFWya8RlBTzHuikfFS1gP49PZ6MYJIVRs8p9YnnKTy7TVcGKY3XZMCA mwIDAQAB -----END PUBLIC KEY----- ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/templates/authorization.html ================================================ 确认您的授权信息

    确认应用的授权信息
    当前应用将会获取您的以下权限: 确认授权
    ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/templates/common/common.html ================================================
    ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/templates/error.html ================================================ 发送了点小错误

    404 找不到页面

    ~~~

    点击返回
    ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/templates/login.html ================================================ 欢迎登录
    欢迎登录

    {{infoText}}

    {{previousText}} 下一步 登录
    ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/templates/logout.html ================================================ 确认退出吗?
    确认退出当前应用吗?
    确认退出
    ================================================ FILE: demo-oauth/oauth-authorization-server/src/main/resources/templates/registerTemplate.html ================================================
    云课程考试平台

    亲爱的用户,你好!

    欢迎您注册 云课程考试平台

    你的邮件的验证码: 验证码
    (请输入该验证码完成 验证,验证码 10 分钟内有效!)

    如果您未申请云课程学习平台 $(type) 服务,请忽略该邮件。

    如果仍有问题,请联系我们的管理员: 000-00000000

    ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java ================================================ package com.xkcoding.oauth; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * . * * @author EchoCow * @date 2020-01-06 15:51 */ public class PasswordEncodeTest { private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Test public void getPasswordWhenPassed() { System.out.println(passwordEncoder.encode("oauth2")); System.out.println(passwordEncoder.encode("123456")); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java ================================================ package com.xkcoding.oauth.oauth; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.xkcoding.oauth.oauth.AuthorizationServerInfo.getUrl; import static org.junit.jupiter.api.Assertions.*; /** * 授权码模式测试. * * @author EchoCow * @date 2020-01-06 20:43 */ public class AuthorizationCodeGrantTests { private AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails(); private AuthorizationServerInfo authorizationServerInfo = new AuthorizationServerInfo(); @BeforeEach void setUp() { resource.setAccessTokenUri(getUrl("/oauth/token")); resource.setClientId("oauth2"); resource.setId("oauth2"); resource.setScope(Arrays.asList("READ", "WRITE")); resource.setAccessTokenUri(getUrl("/oauth/token")); resource.setUserAuthorizationUri(getUrl("/oauth/authorize")); } @Test void testCannotConnectWithoutToken() { OAuth2RestTemplate template = new OAuth2RestTemplate(resource); assertThrows(UserRedirectRequiredException.class, () -> template.getForObject(getUrl("/oauth/me"), String.class)); } @Test void testAttemptedTokenAcquisitionWithNoRedirect() { AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider(); assertThrows(UserRedirectRequiredException.class, () -> provider.obtainAccessToken(resource, new DefaultAccessTokenRequest())); } /** * 这里不使用他提供的是因为很多地方不符合我们的需要 * 比如 csrf,比如许多有些是自己自定义的端点这些 * 所以只有我们一步一步的来进行测试拿到授权码 */ @Test void testCodeAcquisitionWithCorrectContext() { // 1. 请求登录页面获取 _csrf 的 value 以及 cookie ResponseEntity page = authorizationServerInfo.getForString("/oauth/login"); assertNotNull(page.getBody()); String cookie = page.getHeaders().getFirst("Set-Cookie"); HttpHeaders headers = new HttpHeaders(); headers.set("Cookie", cookie); Matcher matcher = Pattern.compile("(?s).*name=\"_csrf\".*?value=\"([^\"]+).*").matcher(page.getBody()); assertTrue(matcher.find()); // 2. 添加表单数据 MultiValueMap form = new LinkedMultiValueMap<>(); form.add("username", "admin"); form.add("password", "123456"); form.add("_csrf", matcher.group(1)); // 3. 登录授权并获取登录成功的 cookie ResponseEntity response = authorizationServerInfo.postForStatus("/authorization/form", headers, form); assertNotNull(response); cookie = response.getHeaders().getFirst("Set-Cookie"); headers = new HttpHeaders(); headers.set("Cookie", cookie); headers.setAccept(Collections.singletonList(MediaType.ALL)); // 4. 请求到 确认授权页面 ,获取确认授权页面的 _csrf 的 value ResponseEntity confirm = authorizationServerInfo.getForString("/oauth/authorize?response_type=code&client_id=oauth2&redirect_uri=http://example.com&scope=READ", headers); headers = confirm.getHeaders(); // 确认过一次后,后面都会自动确认了,这里判断下是不是重定向请求 // 如果不是,就表示是第一次,需要确认授权 if (!confirm.getStatusCode().is3xxRedirection()) { assertNotNull(confirm.getBody()); Matcher matcherConfirm = Pattern.compile("(?s).*name=\"_csrf\".*?value=\"([^\"]+).*").matcher(confirm.getBody()); assertTrue(matcherConfirm.find()); headers = new HttpHeaders(); headers.set("Cookie", cookie); headers.setAccept(Collections.singletonList(MediaType.ALL)); // 5. 构建 同意授权 的表单 form = new LinkedMultiValueMap<>(); form.add("user_oauth_approval", "true"); form.add("scope.READ", "true"); form.add("_csrf", matcherConfirm.group(1)); // 6. 请求授权,获取 授权码 headers = authorizationServerInfo.postForHeaders("/oauth/authorize", form, headers); } URI location = headers.getLocation(); assertNotNull(location); String query = location.getQuery(); assertNotNull(query); String[] result = query.split("="); assertEquals(2, result.length); System.out.println(result[1]); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java ================================================ package com.xkcoding.oauth.oauth; import org.springframework.http.*; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.net.HttpURLConnection; /** * 授权服务器工具类. * * @author EchoCow * @date 2020-01-06 20:44 */ @SuppressWarnings("all") public class AuthorizationServerInfo { public static final String HOST = "http://127.0.0.1:8080"; private RestTemplate client; public AuthorizationServerInfo() { client = new RestTemplate(); client.setRequestFactory(new SimpleClientHttpRequestFactory() { @Override protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { super.prepareConnection(connection, httpMethod); connection.setInstanceFollowRedirects(false); } }); client.setErrorHandler(new ResponseErrorHandler() { public boolean hasError(ClientHttpResponse response) { return false; } public void handleError(ClientHttpResponse response) { } }); } public ResponseEntity getForString(String path, final HttpHeaders headers) { return client.exchange(getUrl(path), HttpMethod.GET, new HttpEntity<>(null, headers), String.class); } public ResponseEntity getForString(String path) { return getForString(path, new HttpHeaders()); } public ResponseEntity postForStatus(String path, HttpHeaders headers, MultiValueMap formData) { HttpHeaders actualHeaders = new HttpHeaders(); actualHeaders.putAll(headers); actualHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); return client.exchange(getUrl(path), HttpMethod.POST, new HttpEntity<>(formData, actualHeaders), (Class) null); } public static String getUrl(String path) { return HOST + path; } public HttpHeaders postForHeaders(String path, MultiValueMap formData, final HttpHeaders headers) { RequestCallback requestCallback = new NullRequestCallback(); if (headers != null) { requestCallback = request -> request.getHeaders().putAll(headers); } StringBuilder builder = new StringBuilder(getUrl(path)); if (!path.contains("?")) { builder.append("?"); } else { builder.append("&"); } for (String key : formData.keySet()) { for (String value : formData.get(key)) { builder.append(key).append("=").append(value); builder.append("&"); } } builder.deleteCharAt(builder.length() - 1); return client.execute(builder.toString(), HttpMethod.POST, requestCallback, HttpMessage::getHeaders); } private static final class NullRequestCallback implements RequestCallback { public void doWithRequest(ClientHttpRequest request) { } } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java ================================================ package com.xkcoding.oauth.oauth; import org.junit.jupiter.api.Test; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; import org.springframework.security.oauth2.common.OAuth2AccessToken; import java.util.Arrays; import static com.xkcoding.oauth.oauth.AuthorizationServerInfo.getUrl; import static org.junit.jupiter.api.Assertions.assertNotNull; /** * . * * @author EchoCow * @date 2020-01-06 21:14 */ public class ResourceOwnerPasswordGrantTests { @Test void testConnectDirectlyToResourceServer() { assertNotNull(accessToken()); } public static String accessToken() { ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails(); resource.setAccessTokenUri(getUrl("/oauth/token")); resource.setClientId("oauth2"); resource.setClientSecret("oauth2"); resource.setId("oauth2"); resource.setScope(Arrays.asList("READ", "WRITE")); resource.setUsername("admin"); resource.setPassword("123456"); OAuth2RestTemplate template = new OAuth2RestTemplate(resource); OAuth2AccessToken accessToken = template.getAccessToken(); return accessToken.getValue(); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java ================================================ package com.xkcoding.oauth.repostiory; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import static org.junit.jupiter.api.Assertions.assertNotNull; /** * . * * @author EchoCow * @date 2020-01-06 13:10 */ @DataJpaTest public class SysClientDetailsTest { @Autowired private SysClientDetailsRepository sysClientDetailsRepository; @Test public void autowiredSuccessWhenPassed() { assertNotNull(sysClientDetailsRepository); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java ================================================ package com.xkcoding.oauth.repostiory; import com.xkcoding.oauth.entity.SysUser; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; /** * . * * @author EchoCow * @date 2020-01-06 13:25 */ @DataJpaTest public class SysUserRepositoryTest { @Autowired private SysUserRepository sysUserRepository; @Test public void autowiredSuccessWhenPassed() { assertNotNull(sysUserRepository); } @Test @DisplayName("测试关联查询") public void queryUserAndRoleWhenPassed() { Optional admin = sysUserRepository.findFirstByUsername("admin"); assertTrue(admin.isPresent()); SysUser sysUser = admin.orElseGet(SysUser::new); assertNotNull(sysUser.getRoles()); assertEquals(1, sysUser.getRoles().size()); } } ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: datasource: url: jdbc:h2:mem:oauth2?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: root password: 123456 jpa: hibernate: ddl-auto: create-drop show-sql: true properties: hibernate: format_sql: true logging: level: org.springframework.security: debug ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/resources/import.sql ================================================ -- 测试数据 INSERT INTO sys_client_details (id, access_token_validity_seconds, authorizations, auto_approve_scopes, client_id, client_secret, grant_types, redirect_url, refresh_token_validity_seconds, resource_ids, scopes) VALUES (1, 6000, null, null, 'oauth2', '$2a$10$O8uM8kd5SbsuoITG3tBifOcarqqI8GP19vzbqDzVHP5ZV9yOfvpYS', 'authorization_code,password', 'http://example.com', 6000, 'oauth2', 'READ,WRITE'); INSERT INTO sys_client_details (id, access_token_validity_seconds, authorizations, auto_approve_scopes, client_id, client_secret, grant_types, redirect_url, refresh_token_validity_seconds, resource_ids, scopes) VALUES (2, 6000, null, null, 'test', '$2a$10$O8uM8kd5SbsuoITG3tBifOcarqqI8GP19vzbqDzVHP5ZV9yOfvpYS', 'authorization_code,password', 'http://example.com', 6000, 'test', 'READ'); INSERT INTO sys_client_details (id, access_token_validity_seconds, authorizations, auto_approve_scopes, client_id, client_secret, grant_types, redirect_url, refresh_token_validity_seconds, resource_ids, scopes) VALUES (3, 6000, null, null, 'test', '$2a$10$O8uM8kd5SbsuoITG3tBifOcarqqI8GP19vzbqDzVHP5ZV9yOfvpYS', 'authorization_code,password', 'http://example.com', 6000, 'error', 'READ'); INSERT INTO sys_role (id, name, description) VALUES (1, 'ROLE_ADMIN', '管理员'); INSERT INTO sys_role (id, name, description) VALUES (2, 'ROLE_TEST', '测试'); INSERT INTO sys_user (id, username, password) VALUES (1, 'admin', '$2a$10$xLH.pDNz3d2frOBQ6Gc.wuHY4ghwlSyFDgy0Ta.psXmm1YJjNaV1G'); INSERT INTO sys_user (id, username, password) VALUES (2, 'test', '$2a$10$xLH.pDNz3d2frOBQ6Gc.wuHY4ghwlSyFDgy0Ta.psXmm1YJjNaV1G'); INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1); INSERT INTO sys_user_role (user_id, role_id) VALUES (2, 2); ================================================ FILE: demo-oauth/oauth-authorization-server/src/test/resources/schema.sql ================================================ create table sys_client_details ( id bigint auto_increment primary key, access_token_validity_seconds int null, authorizations varchar(255) null, auto_approve_scopes varchar(255) null, client_id varchar(255) null, client_secret varchar(255) null, grant_types varchar(255) null, redirect_url varchar(255) null, refresh_token_validity_seconds int null, resource_ids varchar(255) null, scopes varchar(255) null ); create table sys_role ( id bigint auto_increment primary key, name varchar(55) not null, description varchar(55) null ); create table sys_user ( id bigint auto_increment primary key, username varchar(55) not null, password varchar(128) not null ); create table sys_user_role ( id bigint auto_increment primary key, user_id bigint not null, role_id bigint not null, constraint sys_user_role_sys_role_id_fk foreign key (role_id) references sys_role (id), constraint sys_user_role_sys_user_id_fk foreign key (user_id) references sys_user (id) ); ================================================ FILE: demo-oauth/oauth-resource-server/README.adoc ================================================ = spring-boot-demo-oauth-resource-server Doc Writer v1.0, 2019-01-09 :toc: spring boot oauth2 资源服务器,同 授权服务器 一起使用。 > 使用 `spring security oauth` - JWT 解密,远程公钥获取 - 基于角色访问控制 - 基于应用授权域访问控制 == jwt 解密 要先获取 jwt 公钥 [source,java] .OauthResourceTokenConfig ---- public class OauthResourceTokenConfig { // ...... private String getPubKey() { // 如果本地没有密钥,就从授权服务器中获取 return StringUtils.isEmpty(resourceServerProperties.getJwt().getKeyValue()) ? getKeyFromAuthorizationServer() : resourceServerProperties.getJwt().getKeyValue(); } // ...... } ---- 然后配置进去 [source, java] .OauthResourceServerConfig ---- public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { resources .tokenStore(tokenStore) .resourceId(resourceServerProperties.getResourceId()); } } ---- == 访问控制 通过 `@EnableGlobalMethodSecurity(prePostEnabled = true)` 注解开启 `spring security` 的全局方法安全控制 - `@PreAuthorize("hasRole('ADMIN')")` 校验角色 - `@PreAuthorize("#oauth2.hasScope('READ')")` 校验令牌授权域 == 测试 测试用例: `com.xkcoding.oauth.controller.TestControllerTest` 先获取 `token`,携带 `token` 去访问资源即可。 ================================================ FILE: demo-oauth/oauth-resource-server/pom.xml ================================================ demo-oauth com.xkcoding 1.0.0-SNAPSHOT 4.0.0 oauth-resource-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} ================================================ FILE: demo-oauth/oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java ================================================ package com.xkcoding.oauth; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; /** * 启动器. * * @author EchoCow * @version V1.0 * @date 2020-01-09 11:38 */ @EnableResourceServer @SpringBootApplication public class SpringBootDemoResourceApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoResourceApplication.class, args); } } ================================================ FILE: demo-oauth/oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java ================================================ package com.xkcoding.oauth.config; import lombok.AllArgsConstructor; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; /** * 资源服务器配置. * 我们自己实现了它的配置,所以它的自动装配不会生效 * * @author EchoCow * @date 2020-01-09 14:20 */ @Configuration @AllArgsConstructor @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter { private final ResourceServerProperties resourceServerProperties; private final TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.tokenStore(tokenStore).resourceId(resourceServerProperties.getResourceId()); } @Override public void configure(HttpSecurity http) throws Exception { super.configure(http); // 前后端分离下,可以关闭 csrf http.csrf().disable(); } } ================================================ FILE: demo-oauth/oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java ================================================ package com.xkcoding.oauth.config; import cn.hutool.json.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.util.Base64; /** * token 相关配置,jwt 相关. * * @author EchoCow * @date 2020-01-09 14:39 */ @Slf4j @Configuration @AllArgsConstructor public class OauthResourceTokenConfig { private final ResourceServerProperties resourceServerProperties; /** * 这里并不是对令牌的存储,他将访问令牌与身份验证进行转换 * 在需要 {@link TokenStore} 的任何地方可以使用此方法 * * @return TokenStore */ @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } /** * jwt 令牌转换 * * @return jwt */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 非对称密钥加密,获取 public key。 * 自动选择加载方式。 * * @return public key */ private String getPubKey() { // 如果本地没有密钥,就从授权服务器中获取 return StringUtils.isEmpty(resourceServerProperties.getJwt().getKeyValue()) ? getKeyFromAuthorizationServer() : resourceServerProperties.getJwt().getKeyValue(); } /** * 本地没有公钥的时候,从服务器上获取 * 需要进行 Basic 认证 * * @return public key */ private String getKeyFromAuthorizationServer() { ObjectMapper objectMapper = new ObjectMapper(); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.AUTHORIZATION, encodeClient()); HttpEntity requestEntity = new HttpEntity<>(null, httpHeaders); String pubKey = new RestTemplate().getForObject(resourceServerProperties.getJwt().getKeyUri(), String.class, requestEntity); try { JSONObject body = objectMapper.readValue(pubKey, JSONObject.class); log.info("Get Key From Authorization Server."); return body.getStr("value"); } catch (IOException e) { log.error("Get public key error: {}", e.getMessage()); } return null; } /** * 客户端信息 * * @return basic */ private String encodeClient() { return "Basic " + Base64.getEncoder().encodeToString((resourceServerProperties.getClientId() + ":" + resourceServerProperties.getClientSecret()).getBytes()); } } ================================================ FILE: demo-oauth/oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java ================================================ package com.xkcoding.oauth.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试接口. * * @author EchoCow * @date 2020-01-09 14:37 */ @RestController public class TestController { /** * 拥有 ROLE_ADMIN 的用户才能访问的资源 * * @return ADMIN */ @PreAuthorize("hasRole('ADMIN')") @GetMapping("/admin") public String admin() { return "ADMIN"; } /** * 拥有 ROLE_TEST 的用户才能访问的资源 * * @return TEST */ @PreAuthorize("hasRole('TEST')") @GetMapping("/test") public String test() { return "TEST"; } /** * scope 有 READ 的用户资源才能访问 * * @return READ */ @PreAuthorize("#oauth2.hasScope('READ')") @GetMapping("/read") public String read() { return "READ"; } /** * scope 有 WRITE 的用户资源才能访问 * * @return WRITE */ @PreAuthorize("#oauth2.hasScope('WRITE')") @GetMapping("/write") public String write() { return "WRITE"; } } ================================================ FILE: demo-oauth/oauth-resource-server/src/main/resources/application.yml ================================================ server: port: 8081 security: oauth2: resource: token-info-uri: http://localhost:8080/oauth/check_token jwt: key-alias: oauth2 # 如果没有此项会去请求授权服务器获取 key-value: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkF9SyMHeGAsLMwbPsKj/ xpEtS0iCe8vTSBnIGBDZKmB3ma20Ry0Uzn3m+f40RwCXlxnUcvTw7ipoz0tMQERQ b3X4DkYCJXPK6pAD+R9/J5odEwrO2eysByWfcbMjsZw2u5pH5hleMS0YqkrGQOxJ pzlEcKxMePU5KYTbKUJkhOYPY+gQr61g6lF97WggSPtuQn1srT+Ptvfw6yRC4bdI 0zV5emfXjmoLUwaQTRoGYhOFrm97vpoKiltSNIDFW01J1Lr+l77ddDFC6cdiAC0H 5/eENWBBBTFWya8RlBTzHuikfFS1gP49PZ6MYJIVRs8p9YnnKTy7TVcGKY3XZMCA mwIDAQAB -----END PUBLIC KEY----- key-uri: http://localhost:8080/oauth/token_key id: oauth2 client: client-id: oauth2 client-secret: oauth2 access-token-uri: http://localhost:8080/oauth/token scope: READ logging: level: org.springframework.security: debug ================================================ FILE: demo-oauth/oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java ================================================ package com.xkcoding.oauth; import org.junit.jupiter.api.Test; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertNotNull; /** * . * * @author EchoCow * @date 2020-01-09 15:44 */ public class AuthorizationTest { public static final String AUTHORIZATION_SERVER = "http://127.0.0.1:8080"; protected OAuth2RestTemplate oauth2RestTemplate(String username, String password, List scope) { ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails(); resource.setAccessTokenUri(AUTHORIZATION_SERVER + "/oauth/token"); resource.setClientId("oauth2"); resource.setClientSecret("oauth2"); resource.setId("oauth2"); resource.setScope(scope); resource.setUsername(username); resource.setPassword(password); return new OAuth2RestTemplate(resource); } @Test void testAccessTokenWhenPassed() { assertNotNull(oauth2RestTemplate("admin", "123456", Collections.singletonList("READ")).getAccessToken()); } } ================================================ FILE: demo-oauth/oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java ================================================ package com.xkcoding.oauth.controller; import com.xkcoding.oauth.AuthorizationTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; import java.util.Arrays; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.http.HttpMethod.GET; /** * . * * @author EchoCow * @date 2020-01-09 15:46 */ public class TestControllerTest extends AuthorizationTest { private static final String URL = "http://127.0.0.1:8081"; @Test @DisplayName("ROLE_ADMIN 角色测试") void testAdminRoleSucceedAndTestRoleFailedWhenPassed() { OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("READ")); ResponseEntity response = template.exchange(URL + "/admin", GET, null, String.class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("ADMIN", response.getBody()); assertThrows(OAuth2AccessDeniedException.class, () -> template.exchange(URL + "/test", GET, null, String.class)); } @Test @DisplayName("ROLE_Test 角色测试") void testTestRoleSucceedWhenPassed() { OAuth2RestTemplate template = oauth2RestTemplate("test", "123456", Collections.singletonList("READ")); ResponseEntity response = template.exchange(URL + "/test", GET, null, String.class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("TEST", response.getBody()); assertThrows(OAuth2AccessDeniedException.class, () -> template.exchange(URL + "/admin", GET, null, String.class)); } @Test @DisplayName("SCOPE_READ 授权域测试") void testScopeReadWhenPassed() { OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("READ")); ResponseEntity response = template.exchange(URL + "/read", GET, null, String.class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("READ", response.getBody()); assertThrows(OAuth2AccessDeniedException.class, () -> template.exchange(URL + "/write", GET, null, String.class)); } @Test @DisplayName("SCOPE_WRITE 授权域测试") void testScopeWriteWhenPassed() { OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("WRITE")); ResponseEntity response = template.exchange(URL + "/write", GET, null, String.class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("WRITE", response.getBody()); assertThrows(OAuth2AccessDeniedException.class, () -> template.exchange(URL + "/read", GET, null, String.class)); } @Test @DisplayName("SCOPE 测试") void testScopeWhenPassed() { OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Arrays.asList("READ", "WRITE")); ResponseEntity writeResponse = template.exchange(URL + "/write", GET, null, String.class); assertEquals(HttpStatus.OK, writeResponse.getStatusCode()); assertEquals("WRITE", writeResponse.getBody()); ResponseEntity readResponse = template.exchange(URL + "/read", GET, null, String.class); assertEquals(HttpStatus.OK, readResponse.getStatusCode()); assertEquals("READ", readResponse.getBody()); } } ================================================ FILE: demo-oauth/pom.xml ================================================ 4.0.0 demo-oauth 1.0.0-SNAPSHOT oauth-authorization-server oauth-resource-server pom demo-oauth Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 mysql mysql-connector-java runtime com.h2database h2 test org.springframework.boot spring-boot-starter-test test junit junit cn.hutool hutool-all org.projectlombok lombok true org.junit.jupiter junit-jupiter 5.5.2 test demo-oauth org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-beetlsql/.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-orm-beetlsql/README.md ================================================ # spring-boot-demo-orm-beetlsql > 此 demo 主要演示了 Spring Boot 如何整合 beetl sql 快捷操作数据库,使用的是beetl官方提供的beetl-framework-starter集成。集成过程不是十分顺利,没有其他的orm框架集成的便捷。 ## pom.xml ```xml 4.0.0 spring-boot-demo-orm-beetlsql 1.0.0-SNAPSHOT jar spring-boot-demo-orm-beetlsql Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.1.68.RELEASE org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-jdbc com.ibeetl beetl-framework-starter ${ibeetl.version} org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true spring-boot-demo-orm-beetlsql org.springframework.boot spring-boot-maven-plugin ``` ## application.yml > 注意下方注释的地方,**不能解开注释,并且需要通过JavaConfig的方式手动配置数据源**,否则,会导致beetl启动失败,因此,初始化数据库的数据,只能手动在数据库使用 resources/db 下的建表语句和数据库初始化数据。 ```yaml spring: datasource: 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 #### beetlsql starter不能开启下面选项 # type: com.zaxxer.hikari.HikariDataSource # initialization-mode: always # continue-on-error: true # schema: # - "classpath:db/schema.sql" # data: # - "classpath:db/data.sql" # hikari: # minimum-idle: 5 # connection-test-query: SELECT 1 FROM DUAL # maximum-pool-size: 20 # auto-commit: true # idle-timeout: 30000 # pool-name: SpringBootDemoHikariCP # max-lifetime: 60000 # connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.beetlsql: trace beetl: enabled: false beetlsql: enabled: true sqlPath: /sql daoSuffix: Dao basePackage: com.xkcoding.orm.beetlsql.dao dbStyle: org.beetl.sql.core.db.MySqlStyle nameConversion: org.beetl.sql.core.UnderlinedNameConversion beet-beetlsql: dev: true ``` ## BeetlConfig.java ```java /** *

    * Beetl数据源配置 *

    * * @author yangkai.shen * @date Created in 2018-11-14 17:15 */ @Configuration public class BeetlConfig { /** * Beetl需要显示的配置数据源,方可启动项目,大坑,切记! */ @Bean(name = "datasource") public DataSource getDataSource(Environment env){ HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName(env.getProperty("spring.datasource.driver-class-name")); dataSource.setJdbcUrl(env.getProperty("spring.datasource.url")); dataSource.setUsername(env.getProperty("spring.datasource.username")); dataSource.setPassword(env.getProperty("spring.datasource.password")); return dataSource; } } ``` ## UserDao.java ```java /** *

    * UserDao *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:18 */ @Component public interface UserDao extends BaseMapper { } ``` ## UserServiceImpl.java ```java /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:28 */ @Service @Slf4j public class UserServiceImpl implements UserService { private final UserDao userDao; @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } /** * 新增用户 * * @param user 用户 */ @Override public User saveUser(User user) { userDao.insert(user, true); return user; } /** * 批量插入用户 * * @param users 用户列表 */ @Override public void saveUserList(List users) { userDao.insertBatch(users); } /** * 根据主键删除用户 * * @param id 主键 */ @Override public void deleteUser(Long id) { userDao.deleteById(id); } /** * 更新用户 * * @param user 用户 * @return 更新后的用户 */ @Override public User updateUser(User user) { if (ObjectUtil.isNull(user)) { throw new RuntimeException("用户id不能为null"); } userDao.updateTemplateById(user); return userDao.single(user.getId()); } /** * 查询单个用户 * * @param id 主键id * @return 用户信息 */ @Override public User getUser(Long id) { return userDao.single(id); } /** * 查询用户列表 * * @return 用户列表 */ @Override public List getUserList() { return userDao.all(); } /** * 分页查询 * * @param currentPage 当前页 * @param pageSize 每页条数 * @return 分页用户列表 */ @Override public PageQuery getUserByPage(Integer currentPage, Integer pageSize) { return userDao.createLambdaQuery().page(currentPage, pageSize); } } ``` ## UserServiceTest.java ```java /** *

    * User Service测试 *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:30 */ @Slf4j public class UserServiceTest extends SpringBootDemoOrmBeetlsqlApplicationTests { @Autowired private UserService userService; @Test public void saveUser() { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); user = userService.saveUser(user); Assert.assertTrue(ObjectUtil.isNotNull(user.getId())); log.debug("【user】= {}", user); } @Test public void saveUserList() { List users = Lists.newArrayList(); for (int i = 5; i < 15; i++) { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave" + i).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + i + "@xkcoding.com").phoneNumber("1730000000" + i).status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); users.add(user); } userService.saveUserList(users); Assert.assertTrue(userService.getUserList().size() > 2); } @Test public void deleteUser() { userService.deleteUser(1L); User user = userService.getUser(1L); Assert.assertTrue(ObjectUtil.isNull(user)); } @Test public void updateUser() { User user = userService.getUser(2L); user.setName("beetlSql 修改后的名字"); User update = userService.updateUser(user); Assert.assertEquals("beetlSql 修改后的名字", update.getName()); log.debug("【update】= {}", update); } @Test public void getUser() { User user = userService.getUser(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } @Test public void getUserList() { List userList = userService.getUserList(); Assert.assertTrue(CollUtil.isNotEmpty(userList)); log.debug("【userList】= {}", userList); } @Test public void getUserByPage() { List userList = userService.getUserList(); PageQuery userByPage = userService.getUserByPage(1, 5); Assert.assertEquals(5, userByPage.getList().size()); Assert.assertEquals(userList.size(), userByPage.getTotalRow()); log.debug("【userByPage】= {}", JSONUtil.toJsonStr(userByPage)); } } ``` ## 参考 - BeetlSQL官方文档:http://ibeetl.com/guide/#beetlsql - 开源项目:https://gitee.com/yangkb/springboot-beetl-beetlsql - 博客:https://blog.csdn.net/flystarfly/article/details/82752597 ================================================ FILE: demo-orm-beetlsql/pom.xml ================================================ 4.0.0 demo-orm-beetlsql 1.0.0-SNAPSHOT jar demo-orm-beetlsql Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.1.68.RELEASE org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-jdbc com.ibeetl beetl-framework-starter ${ibeetl.version} org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-orm-beetlsql org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-beetlsql/src/main/java/com/xkcoding/orm/beetlsql/SpringBootDemoOrmBeetlsqlApplication.java ================================================ package com.xkcoding.orm.beetlsql; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-11-14 15:47 */ @SpringBootApplication public class SpringBootDemoOrmBeetlsqlApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmBeetlsqlApplication.class, args); } } ================================================ FILE: demo-orm-beetlsql/src/main/java/com/xkcoding/orm/beetlsql/config/BeetlConfig.java ================================================ package com.xkcoding.orm.beetlsql.config; import com.zaxxer.hikari.HikariDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import javax.sql.DataSource; /** *

    * Beetl数据源配置 *

    * * @author yangkai.shen * @date Created in 2018-11-14 17:15 */ @Configuration public class BeetlConfig { /** * Beetl需要显示的配置数据源,方可启动项目,大坑,切记! */ @Bean(name = "datasource") public DataSource getDataSource(Environment env) { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName(env.getProperty("spring.datasource.driver-class-name")); dataSource.setJdbcUrl(env.getProperty("spring.datasource.url")); dataSource.setUsername(env.getProperty("spring.datasource.username")); dataSource.setPassword(env.getProperty("spring.datasource.password")); return dataSource; } } ================================================ FILE: demo-orm-beetlsql/src/main/java/com/xkcoding/orm/beetlsql/dao/UserDao.java ================================================ package com.xkcoding.orm.beetlsql.dao; import com.xkcoding.orm.beetlsql.entity.User; import org.beetl.sql.core.mapper.BaseMapper; import org.springframework.stereotype.Component; /** *

    * UserDao *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:18 */ @Component public interface UserDao extends BaseMapper { } ================================================ FILE: demo-orm-beetlsql/src/main/java/com/xkcoding/orm/beetlsql/entity/User.java ================================================ package com.xkcoding.orm.beetlsql.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.beetl.sql.core.annotatoin.Table; import java.io.Serializable; import java.util.Date; /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:06 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @Table(name = "orm_user") public class User implements Serializable { private static final long serialVersionUID = -1840831686851699943L; /** * 主键 */ private Long id; /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 创建时间 */ private Date createTime; /** * 上次登录时间 */ private Date lastLoginTime; /** * 上次更新时间 */ private Date lastUpdateTime; } ================================================ FILE: demo-orm-beetlsql/src/main/java/com/xkcoding/orm/beetlsql/service/UserService.java ================================================ package com.xkcoding.orm.beetlsql.service; import com.xkcoding.orm.beetlsql.entity.User; import org.beetl.sql.core.engine.PageQuery; import java.util.List; /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:18 */ public interface UserService { /** * 新增用户 * * @param user 用户 * @return 保存的用户 */ User saveUser(User user); /** * 批量插入用户 * * @param users 用户列表 */ void saveUserList(List users); /** * 根据主键删除用户 * * @param id 主键 */ void deleteUser(Long id); /** * 更新用户 * * @param user 用户 * @return 更新后的用户 */ User updateUser(User user); /** * 查询单个用户 * * @param id 主键id * @return 用户信息 */ User getUser(Long id); /** * 查询用户列表 * * @return 用户列表 */ List getUserList(); /** * 分页查询 * * @param currentPage 当前页 * @param pageSize 每页条数 * @return 分页用户列表 */ PageQuery getUserByPage(Integer currentPage, Integer pageSize); } ================================================ FILE: demo-orm-beetlsql/src/main/java/com/xkcoding/orm/beetlsql/service/impl/UserServiceImpl.java ================================================ package com.xkcoding.orm.beetlsql.service.impl; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.orm.beetlsql.dao.UserDao; import com.xkcoding.orm.beetlsql.entity.User; import com.xkcoding.orm.beetlsql.service.UserService; import lombok.extern.slf4j.Slf4j; import org.beetl.sql.core.engine.PageQuery; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:28 */ @Service @Slf4j public class UserServiceImpl implements UserService { private final UserDao userDao; @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } /** * 新增用户 * * @param user 用户 */ @Override public User saveUser(User user) { userDao.insert(user, true); return user; } /** * 批量插入用户 * * @param users 用户列表 */ @Override public void saveUserList(List users) { userDao.insertBatch(users); } /** * 根据主键删除用户 * * @param id 主键 */ @Override public void deleteUser(Long id) { userDao.deleteById(id); } /** * 更新用户 * * @param user 用户 * @return 更新后的用户 */ @Override public User updateUser(User user) { if (ObjectUtil.isNull(user)) { throw new RuntimeException("用户id不能为null"); } userDao.updateTemplateById(user); return userDao.single(user.getId()); } /** * 查询单个用户 * * @param id 主键id * @return 用户信息 */ @Override public User getUser(Long id) { return userDao.single(id); } /** * 查询用户列表 * * @return 用户列表 */ @Override public List getUserList() { return userDao.all(); } /** * 分页查询 * * @param currentPage 当前页 * @param pageSize 每页条数 * @return 分页用户列表 */ @Override public PageQuery getUserByPage(Integer currentPage, Integer pageSize) { return userDao.createLambdaQuery().page(currentPage, pageSize); } } ================================================ FILE: demo-orm-beetlsql/src/main/resources/application.yml ================================================ spring: datasource: 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 #### beetlsql starter不能开启下面选项 # type: com.zaxxer.hikari.HikariDataSource # initialization-mode: always # continue-on-error: true # schema: # - "classpath:db/schema.sql" # data: # - "classpath:db/data.sql" # hikari: # minimum-idle: 5 # connection-test-query: SELECT 1 FROM DUAL # maximum-pool-size: 20 # auto-commit: true # idle-timeout: 30000 # pool-name: SpringBootDemoHikariCP # max-lifetime: 60000 # connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.beetlsql: trace beetl: enabled: false beetlsql: enabled: true sqlPath: /sql daoSuffix: Dao basePackage: com.xkcoding.orm.beetlsql.dao dbStyle: org.beetl.sql.core.db.MySqlStyle nameConversion: org.beetl.sql.core.UnderlinedNameConversion beet-beetlsql: dev: true ================================================ FILE: demo-orm-beetlsql/src/main/resources/db/data.sql ================================================ INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (1, 'user_1', 'ff342e862e7c3285cdc07e56d6b8973b', '412365a109674b2dbb1981ed561a4c70', 'user1@xkcoding.com', '17300000001'); INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (2, 'user_2', '6c6bf02c8d5d3d128f34b1700cb1e32c', 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user2@xkcoding.com', '17300000002'); ================================================ FILE: demo-orm-beetlsql/src/main/resources/db/schema.sql ================================================ DROP TABLE IF EXISTS `orm_user`; CREATE TABLE `orm_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(32) NOT NULL COMMENT '加密后的密码', `salt` VARCHAR(32) NOT NULL COMMENT '加密使用的盐', `email` VARCHAR(32) NOT NULL UNIQUE COMMENT '邮箱', `phone_number` VARCHAR(15) NOT NULL UNIQUE COMMENT '手机号码', `status` INT(2) NOT NULL DEFAULT 1 COMMENT '状态,-1:逻辑删除,0:禁用,1:启用', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_login_time` DATETIME DEFAULT NULL COMMENT '上次登录时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; ================================================ FILE: demo-orm-beetlsql/src/test/java/com/xkcoding/orm/beetlsql/SpringBootDemoOrmBeetlsqlApplicationTests.java ================================================ package com.xkcoding.orm.beetlsql; 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 SpringBootDemoOrmBeetlsqlApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-orm-beetlsql/src/test/java/com/xkcoding/orm/beetlsql/service/UserServiceTest.java ================================================ package com.xkcoding.orm.beetlsql.service; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.json.JSONUtil; import com.xkcoding.orm.beetlsql.SpringBootDemoOrmBeetlsqlApplicationTests; import com.xkcoding.orm.beetlsql.entity.User; import lombok.extern.slf4j.Slf4j; import org.assertj.core.util.Lists; import org.beetl.sql.core.engine.PageQuery; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** *

    * User Service测试 *

    * * @author yangkai.shen * @date Created in 2018-11-14 16:30 */ @Slf4j public class UserServiceTest extends SpringBootDemoOrmBeetlsqlApplicationTests { @Autowired private UserService userService; @Test public void saveUser() { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); user = userService.saveUser(user); Assert.assertTrue(ObjectUtil.isNotNull(user.getId())); log.debug("【user】= {}", user); } @Test public void saveUserList() { List users = Lists.newArrayList(); for (int i = 5; i < 15; i++) { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave" + i).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + i + "@xkcoding.com").phoneNumber("1730000000" + i).status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); users.add(user); } userService.saveUserList(users); Assert.assertTrue(userService.getUserList().size() > 2); } @Test public void deleteUser() { userService.deleteUser(1L); User user = userService.getUser(1L); Assert.assertTrue(ObjectUtil.isNull(user)); } @Test public void updateUser() { User user = userService.getUser(2L); user.setName("beetlSql 修改后的名字"); User update = userService.updateUser(user); Assert.assertEquals("beetlSql 修改后的名字", update.getName()); log.debug("【update】= {}", update); } @Test public void getUser() { User user = userService.getUser(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } @Test public void getUserList() { List userList = userService.getUserList(); Assert.assertTrue(CollUtil.isNotEmpty(userList)); log.debug("【userList】= {}", userList); } @Test public void getUserByPage() { List userList = userService.getUserList(); PageQuery userByPage = userService.getUserByPage(1, 5); Assert.assertEquals(5, userByPage.getList().size()); Assert.assertEquals(userList.size(), userByPage.getTotalRow()); log.debug("【userByPage】= {}", JSONUtil.toJsonStr(userByPage)); } } ================================================ FILE: demo-orm-jdbctemplate/.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-orm-jdbctemplate/README.md ================================================ # spring-boot-demo-orm-jdbctemplate > 本 demo 主要演示了Spring Boot如何使用 JdbcTemplate 操作数据库,并且简易地封装了一个通用的 Dao 层,包括增删改查。 ## pom.xml ```xml 4.0.0 spring-boot-demo-orm-jdbctemplate 1.0.0-SNAPSHOT jar spring-boot-demo-orm-jdbctemplate 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-jdbc org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java cn.hutool hutool-all org.projectlombok lombok true spring-boot-demo-orm-jdbctemplate org.springframework.boot spring-boot-maven-plugin ``` ## BaseDao.java ```java /** *

    * Dao基类 *

    * * @author yangkai.shen * @date Created in 2018-10-15 11:28 */ @Slf4j public class BaseDao { private JdbcTemplate jdbcTemplate; private Class clazz; @SuppressWarnings(value = "unchecked") public BaseDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; } /** * 通用插入,自增列需要添加 {@link Pk} 注解 * * @param t 对象 * @param ignoreNull 是否忽略 null 值 * @return 操作的行数 */ protected Integer insert(T t, Boolean ignoreNull) { String table = getTableName(t); List filterField = getField(t, ignoreNull); List columnList = getColumns(filterField); String columns = StrUtil.join(Const.SEPARATOR_COMMA, columnList); // 构造占位符 String params = StrUtil.repeatAndJoin("?", columnList.size(), Const.SEPARATOR_COMMA); // 构造值 Object[] values = filterField.stream().map(field -> ReflectUtil.getFieldValue(t, field)).toArray(); String sql = StrUtil.format("INSERT INTO {table} ({columns}) VALUES ({params})", Dict.create().set("table", table).set("columns", columns).set("params", params)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(values)); return jdbcTemplate.update(sql, values); } /** * 通用根据主键删除 * * @param pk 主键 * @return 影响行数 */ protected Integer deleteById(P pk) { String tableName = getTableName(); String sql = StrUtil.format("DELETE FROM {table} where id = ?", Dict.create().set("table", tableName)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(pk)); return jdbcTemplate.update(sql, pk); } /** * 通用根据主键更新,自增列需要添加 {@link Pk} 注解 * * @param t 对象 * @param pk 主键 * @param ignoreNull 是否忽略 null 值 * @return 操作的行数 */ protected Integer updateById(T t, P pk, Boolean ignoreNull) { String tableName = getTableName(t); List filterField = getField(t, ignoreNull); List columnList = getColumns(filterField); List columns = columnList.stream().map(s -> StrUtil.appendIfMissing(s, " = ?")).collect(Collectors.toList()); String params = StrUtil.join(Const.SEPARATOR_COMMA, columns); // 构造值 List valueList = filterField.stream().map(field -> ReflectUtil.getFieldValue(t, field)).collect(Collectors.toList()); valueList.add(pk); Object[] values = ArrayUtil.toArray(valueList, Object.class); String sql = StrUtil.format("UPDATE {table} SET {params} where id = ?", Dict.create().set("table", tableName).set("params", params)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(values)); return jdbcTemplate.update(sql, values); } /** * 通用根据主键查询单条记录 * * @param pk 主键 * @return 单条记录 */ public T findOneById(P pk) { String tableName = getTableName(); String sql = StrUtil.format("SELECT * FROM {table} where id = ?", Dict.create().set("table", tableName)); RowMapper rowMapper = new BeanPropertyRowMapper<>(clazz); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(pk)); return jdbcTemplate.queryForObject(sql, new Object[]{pk}, rowMapper); } /** * 根据对象查询 * * @param t 查询条件 * @return 对象列表 */ public List findByExample(T t) { String tableName = getTableName(t); List filterField = getField(t, true); List columnList = getColumns(filterField); List columns = columnList.stream().map(s -> " and " + s + " = ? ").collect(Collectors.toList()); String where = StrUtil.join(" ", columns); // 构造值 Object[] values = filterField.stream().map(field -> ReflectUtil.getFieldValue(t, field)).toArray(); String sql = StrUtil.format("SELECT * FROM {table} where 1=1 {where}", Dict.create().set("table", tableName).set("where", StrUtil.isBlank(where) ? "" : where)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(values)); List> maps = jdbcTemplate.queryForList(sql, values); List ret = CollUtil.newArrayList(); maps.forEach(map -> ret.add(BeanUtil.fillBeanWithMap(map, ReflectUtil.newInstance(clazz), true, false))); return ret; } /** * 获取表名 * * @param t 对象 * @return 表名 */ private String getTableName(T t) { Table tableAnnotation = t.getClass().getAnnotation(Table.class); if (ObjectUtil.isNotNull(tableAnnotation)) { return StrUtil.format("`{}`", tableAnnotation.name()); } else { return StrUtil.format("`{}`", t.getClass().getName().toLowerCase()); } } /** * 获取表名 * * @return 表名 */ private String getTableName() { Table tableAnnotation = clazz.getAnnotation(Table.class); if (ObjectUtil.isNotNull(tableAnnotation)) { return StrUtil.format("`{}`", tableAnnotation.name()); } else { return StrUtil.format("`{}`", clazz.getName().toLowerCase()); } } /** * 获取列 * * @param fieldList 字段列表 * @return 列信息列表 */ private List getColumns(List fieldList) { // 构造列 List columnList = CollUtil.newArrayList(); for (Field field : fieldList) { Column columnAnnotation = field.getAnnotation(Column.class); String columnName; if (ObjectUtil.isNotNull(columnAnnotation)) { columnName = columnAnnotation.name(); } else { columnName = field.getName(); } columnList.add(StrUtil.format("`{}`", columnName)); } return columnList; } /** * 获取字段列表 {@code 过滤数据库中不存在的字段,以及自增列} * * @param t 对象 * @param ignoreNull 是否忽略空值 * @return 字段列表 */ private List getField(T t, Boolean ignoreNull) { // 获取所有字段,包含父类中的字段 Field[] fields = ReflectUtil.getFields(t.getClass()); // 过滤数据库中不存在的字段,以及自增列 List filterField; Stream fieldStream = CollUtil.toList(fields).stream().filter(field -> ObjectUtil.isNull(field.getAnnotation(Ignore.class)) || ObjectUtil.isNull(field.getAnnotation(Pk.class))); // 是否过滤字段值为null的字段 if (ignoreNull) { filterField = fieldStream.filter(field -> ObjectUtil.isNotNull(ReflectUtil.getFieldValue(t, field))).collect(Collectors.toList()); } else { filterField = fieldStream.collect(Collectors.toList()); } return filterField; } } ``` ## application.yml ```yaml 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&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug ``` ## 备注 其余详细代码参见 demo ================================================ FILE: demo-orm-jdbctemplate/pom.xml ================================================ 4.0.0 demo-orm-jdbctemplate 1.0.0-SNAPSHOT jar demo-orm-jdbctemplate 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-jdbc org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java cn.hutool hutool-all org.projectlombok lombok true demo-orm-jdbctemplate org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/SpringBootDemoOrmJdbctemplateApplication.java ================================================ package com.xkcoding.orm.jdbctemplate; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-15 9:50 */ @SpringBootApplication public class SpringBootDemoOrmJdbctemplateApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmJdbctemplateApplication.class, args); } } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/annotation/Column.java ================================================ package com.xkcoding.orm.jdbctemplate.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** *

    * 列注解 *

    * * @author yangkai.shen * @date Created in 2018-10-15 11:23 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface Column { /** * 列名 * * @return 列名 */ String name(); } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/annotation/Ignore.java ================================================ package com.xkcoding.orm.jdbctemplate.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** *

    * 需要忽略的字段 *

    * * @author yangkai.shen * @date Created in 2018-10-15 13:25 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface Ignore { } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/annotation/Pk.java ================================================ package com.xkcoding.orm.jdbctemplate.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** *

    * 主键注解 *

    * * @author yangkai.shen * @date Created in 2018-10-15 11:23 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface Pk { /** * 自增 * * @return 自增主键 */ boolean auto() default true; } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/annotation/Table.java ================================================ package com.xkcoding.orm.jdbctemplate.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** *

    * 表注解 *

    * * @author yangkai.shen * @date Created in 2018-10-15 11:23 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface Table { /** * 表名 * * @return 表名 */ String name(); } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/constant/Const.java ================================================ package com.xkcoding.orm.jdbctemplate.constant; /** *

    * 常量池 *

    * * @author yangkai.shen * @date Created in 2018-10-15 10:59 */ public interface Const { /** * 加密盐前缀 */ String SALT_PREFIX = "::SpringBootDemo::"; /** * 逗号分隔符 */ String SEPARATOR_COMMA = ","; } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/controller/UserController.java ================================================ package com.xkcoding.orm.jdbctemplate.controller; import cn.hutool.core.lang.Dict; import com.xkcoding.orm.jdbctemplate.entity.User; import com.xkcoding.orm.jdbctemplate.service.IUserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** *

    * User Controller *

    * * @author yangkai.shen * @date Created in 2018-10-15 13:58 */ @RestController @Slf4j public class UserController { private final IUserService userService; @Autowired public UserController(IUserService userService) { this.userService = userService; } @PostMapping("/user") public Dict save(@RequestBody User user) { Boolean save = userService.save(user); return Dict.create().set("code", save ? 200 : 500).set("msg", save ? "成功" : "失败").set("data", save ? user : null); } @DeleteMapping("/user/{id}") public Dict delete(@PathVariable Long id) { Boolean delete = userService.delete(id); return Dict.create().set("code", delete ? 200 : 500).set("msg", delete ? "成功" : "失败"); } @PutMapping("/user/{id}") public Dict update(@RequestBody User user, @PathVariable Long id) { Boolean update = userService.update(user, id); return Dict.create().set("code", update ? 200 : 500).set("msg", update ? "成功" : "失败").set("data", update ? user : null); } @GetMapping("/user/{id}") public Dict getUser(@PathVariable Long id) { User user = userService.getUser(id); return Dict.create().set("code", 200).set("msg", "成功").set("data", user); } @GetMapping("/user") public Dict getUser(User user) { List userList = userService.getUser(user); return Dict.create().set("code", 200).set("msg", "成功").set("data", userList); } } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/dao/UserDao.java ================================================ package com.xkcoding.orm.jdbctemplate.dao; import com.xkcoding.orm.jdbctemplate.dao.base.BaseDao; import com.xkcoding.orm.jdbctemplate.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; /** *

    * User Dao *

    * * @author yangkai.shen * @date Created in 2018-10-15 11:15 */ @Repository public class UserDao extends BaseDao { @Autowired public UserDao(JdbcTemplate jdbcTemplate) { super(jdbcTemplate); } /** * 保存用户 * * @param user 用户对象 * @return 操作影响行数 */ public Integer insert(User user) { return super.insert(user, true); } /** * 根据主键删除用户 * * @param id 主键id * @return 操作影响行数 */ public Integer delete(Long id) { return super.deleteById(id); } /** * 更新用户 * * @param user 用户对象 * @param id 主键id * @return 操作影响行数 */ public Integer update(User user, Long id) { return super.updateById(user, id, true); } /** * 根据主键获取用户 * * @param id 主键id * @return id对应的用户 */ public User selectById(Long id) { return super.findOneById(id); } /** * 根据查询条件获取用户列表 * * @param user 用户查询条件 * @return 用户列表 */ public List selectUserList(User user) { return super.findByExample(user); } } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/dao/base/BaseDao.java ================================================ package com.xkcoding.orm.jdbctemplate.dao.base; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.xkcoding.orm.jdbctemplate.annotation.Column; import com.xkcoding.orm.jdbctemplate.annotation.Ignore; import com.xkcoding.orm.jdbctemplate.annotation.Pk; import com.xkcoding.orm.jdbctemplate.annotation.Table; import com.xkcoding.orm.jdbctemplate.constant.Const; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; /** *

    * Dao基类 *

    * * @author yangkai.shen * @date Created in 2018-10-15 11:28 */ @Slf4j public class BaseDao { private JdbcTemplate jdbcTemplate; private Class clazz; @SuppressWarnings(value = "unchecked") public BaseDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; } /** * 通用插入,自增列需要添加 {@link Pk} 注解 * * @param t 对象 * @param ignoreNull 是否忽略 null 值 * @return 操作的行数 */ protected Integer insert(T t, Boolean ignoreNull) { String table = getTableName(t); List filterField = getField(t, ignoreNull); List columnList = getColumns(filterField); String columns = StrUtil.join(Const.SEPARATOR_COMMA, columnList); // 构造占位符 String params = StrUtil.repeatAndJoin("?", columnList.size(), Const.SEPARATOR_COMMA); // 构造值 Object[] values = filterField.stream().map(field -> ReflectUtil.getFieldValue(t, field)).toArray(); String sql = StrUtil.format("INSERT INTO {table} ({columns}) VALUES ({params})", Dict.create().set("table", table).set("columns", columns).set("params", params)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(values)); return jdbcTemplate.update(sql, values); } /** * 通用根据主键删除 * * @param pk 主键 * @return 影响行数 */ protected Integer deleteById(P pk) { String tableName = getTableName(); String sql = StrUtil.format("DELETE FROM {table} where id = ?", Dict.create().set("table", tableName)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(pk)); return jdbcTemplate.update(sql, pk); } /** * 通用根据主键更新,自增列需要添加 {@link Pk} 注解 * * @param t 对象 * @param pk 主键 * @param ignoreNull 是否忽略 null 值 * @return 操作的行数 */ protected Integer updateById(T t, P pk, Boolean ignoreNull) { String tableName = getTableName(t); List filterField = getField(t, ignoreNull); List columnList = getColumns(filterField); List columns = columnList.stream().map(s -> StrUtil.appendIfMissing(s, " = ?")).collect(Collectors.toList()); String params = StrUtil.join(Const.SEPARATOR_COMMA, columns); // 构造值 List valueList = filterField.stream().map(field -> ReflectUtil.getFieldValue(t, field)).collect(Collectors.toList()); valueList.add(pk); Object[] values = ArrayUtil.toArray(valueList, Object.class); String sql = StrUtil.format("UPDATE {table} SET {params} where id = ?", Dict.create().set("table", tableName).set("params", params)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(values)); return jdbcTemplate.update(sql, values); } /** * 通用根据主键查询单条记录 * * @param pk 主键 * @return 单条记录 */ public T findOneById(P pk) { String tableName = getTableName(); String sql = StrUtil.format("SELECT * FROM {table} where id = ?", Dict.create().set("table", tableName)); RowMapper rowMapper = new BeanPropertyRowMapper<>(clazz); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(pk)); return jdbcTemplate.queryForObject(sql, new Object[]{pk}, rowMapper); } /** * 根据对象查询 * * @param t 查询条件 * @return 对象列表 */ public List findByExample(T t) { String tableName = getTableName(t); List filterField = getField(t, true); List columnList = getColumns(filterField); List columns = columnList.stream().map(s -> " and " + s + " = ? ").collect(Collectors.toList()); String where = StrUtil.join(" ", columns); // 构造值 Object[] values = filterField.stream().map(field -> ReflectUtil.getFieldValue(t, field)).toArray(); String sql = StrUtil.format("SELECT * FROM {table} where 1=1 {where}", Dict.create().set("table", tableName).set("where", StrUtil.isBlank(where) ? "" : where)); log.debug("【执行SQL】SQL:{}", sql); log.debug("【执行SQL】参数:{}", JSONUtil.toJsonStr(values)); List> maps = jdbcTemplate.queryForList(sql, values); List ret = CollUtil.newArrayList(); maps.forEach(map -> ret.add(BeanUtil.fillBeanWithMap(map, ReflectUtil.newInstance(clazz), true, false))); return ret; } /** * 获取表名 * * @param t 对象 * @return 表名 */ private String getTableName(T t) { Table tableAnnotation = t.getClass().getAnnotation(Table.class); if (ObjectUtil.isNotNull(tableAnnotation)) { return StrUtil.format("`{}`", tableAnnotation.name()); } else { return StrUtil.format("`{}`", t.getClass().getName().toLowerCase()); } } /** * 获取表名 * * @return 表名 */ private String getTableName() { Table tableAnnotation = clazz.getAnnotation(Table.class); if (ObjectUtil.isNotNull(tableAnnotation)) { return StrUtil.format("`{}`", tableAnnotation.name()); } else { return StrUtil.format("`{}`", clazz.getName().toLowerCase()); } } /** * 获取列 * * @param fieldList 字段列表 * @return 列信息列表 */ private List getColumns(List fieldList) { // 构造列 List columnList = CollUtil.newArrayList(); for (Field field : fieldList) { Column columnAnnotation = field.getAnnotation(Column.class); String columnName; if (ObjectUtil.isNotNull(columnAnnotation)) { columnName = columnAnnotation.name(); } else { columnName = field.getName(); } columnList.add(StrUtil.format("`{}`", columnName)); } return columnList; } /** * 获取字段列表 {@code 过滤数据库中不存在的字段,以及自增列} * * @param t 对象 * @param ignoreNull 是否忽略空值 * @return 字段列表 */ private List getField(T t, Boolean ignoreNull) { // 获取所有字段,包含父类中的字段 Field[] fields = ReflectUtil.getFields(t.getClass()); // 过滤数据库中不存在的字段,以及自增列 List filterField; Stream fieldStream = CollUtil.toList(fields).stream().filter(field -> ObjectUtil.isNull(field.getAnnotation(Ignore.class)) || ObjectUtil.isNull(field.getAnnotation(Pk.class))); // 是否过滤字段值为null的字段 if (ignoreNull) { filterField = fieldStream.filter(field -> ObjectUtil.isNotNull(ReflectUtil.getFieldValue(t, field))).collect(Collectors.toList()); } else { filterField = fieldStream.collect(Collectors.toList()); } return filterField; } } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/entity/User.java ================================================ package com.xkcoding.orm.jdbctemplate.entity; import com.xkcoding.orm.jdbctemplate.annotation.Column; import com.xkcoding.orm.jdbctemplate.annotation.Pk; import com.xkcoding.orm.jdbctemplate.annotation.Table; import lombok.Data; import java.io.Serializable; import java.util.Date; /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-10-15 10:45 */ @Data @Table(name = "orm_user") public class User implements Serializable { /** * 主键 */ @Pk private Long id; /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ @Column(name = "phone_number") private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 创建时间 */ @Column(name = "create_time") private Date createTime; /** * 上次登录时间 */ @Column(name = "last_login_time") private Date lastLoginTime; /** * 上次更新时间 */ @Column(name = "last_update_time") private Date lastUpdateTime; } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/service/IUserService.java ================================================ package com.xkcoding.orm.jdbctemplate.service; import com.xkcoding.orm.jdbctemplate.entity.User; import java.util.List; /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-10-15 13:51 */ public interface IUserService { /** * 保存用户 * * @param user 用户实体 * @return 保存成功 {@code true} 保存失败 {@code false} */ Boolean save(User user); /** * 删除用户 * * @param id 主键id * @return 删除成功 {@code true} 删除失败 {@code false} */ Boolean delete(Long id); /** * 更新用户 * * @param user 用户实体 * @param id 主键id * @return 更新成功 {@code true} 更新失败 {@code false} */ Boolean update(User user, Long id); /** * 获取单个用户 * * @param id 主键id * @return 单个用户对象 */ User getUser(Long id); /** * 获取用户列表 * * @param user 用户实体 * @return 用户列表 */ List getUser(User user); } ================================================ FILE: demo-orm-jdbctemplate/src/main/java/com/xkcoding/orm/jdbctemplate/service/impl/UserServiceImpl.java ================================================ package com.xkcoding.orm.jdbctemplate.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import com.xkcoding.orm.jdbctemplate.constant.Const; import com.xkcoding.orm.jdbctemplate.dao.UserDao; import com.xkcoding.orm.jdbctemplate.entity.User; import com.xkcoding.orm.jdbctemplate.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** *

    * User Service Implement *

    * * @author yangkai.shen * @date Created in 2018-10-15 13:53 */ @Service public class UserServiceImpl implements IUserService { private final UserDao userDao; @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } /** * 保存用户 * * @param user 用户实体 * @return 保存成功 {@code true} 保存失败 {@code false} */ @Override public Boolean save(User user) { String rawPass = user.getPassword(); String salt = IdUtil.simpleUUID(); String pass = SecureUtil.md5(rawPass + Const.SALT_PREFIX + salt); user.setPassword(pass); user.setSalt(salt); return userDao.insert(user) > 0; } /** * 删除用户 * * @param id 主键id * @return 删除成功 {@code true} 删除失败 {@code false} */ @Override public Boolean delete(Long id) { return userDao.delete(id) > 0; } /** * 更新用户 * * @param user 用户实体 * @param id 主键id * @return 更新成功 {@code true} 更新失败 {@code false} */ @Override public Boolean update(User user, Long id) { User exist = getUser(id); if (StrUtil.isNotBlank(user.getPassword())) { String rawPass = user.getPassword(); String salt = IdUtil.simpleUUID(); String pass = SecureUtil.md5(rawPass + Const.SALT_PREFIX + salt); user.setPassword(pass); user.setSalt(salt); } BeanUtil.copyProperties(user, exist, CopyOptions.create().setIgnoreNullValue(true)); exist.setLastUpdateTime(new DateTime()); return userDao.update(exist, id) > 0; } /** * 获取单个用户 * * @param id 主键id * @return 单个用户对象 */ @Override public User getUser(Long id) { return userDao.findOneById(id); } /** * 获取用户列表 * * @param user 用户实体 * @return 用户列表 */ @Override public List getUser(User user) { return userDao.findByExample(user); } } ================================================ FILE: demo-orm-jdbctemplate/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&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug ================================================ FILE: demo-orm-jdbctemplate/src/main/resources/db/data.sql ================================================ INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (1, 'user_1', 'ff342e862e7c3285cdc07e56d6b8973b', '412365a109674b2dbb1981ed561a4c70', 'user1@xkcoding.com', '17300000001'); INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (2, 'user_2', '6c6bf02c8d5d3d128f34b1700cb1e32c', 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user2@xkcoding.com', '17300000002'); ================================================ FILE: demo-orm-jdbctemplate/src/main/resources/db/schema.sql ================================================ DROP TABLE IF EXISTS `orm_user`; CREATE TABLE `orm_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(32) NOT NULL COMMENT '加密后的密码', `salt` VARCHAR(32) NOT NULL COMMENT '加密使用的盐', `email` VARCHAR(32) NOT NULL UNIQUE COMMENT '邮箱', `phone_number` VARCHAR(15) NOT NULL UNIQUE COMMENT '手机号码', `status` INT(2) NOT NULL DEFAULT 1 COMMENT '状态,-1:逻辑删除,0:禁用,1:启用', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_login_time` DATETIME DEFAULT NULL COMMENT '上次登录时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; ================================================ FILE: demo-orm-jdbctemplate/src/test/java/com/xkcoding/orm/jdbctemplate/SpringBootDemoOrmJdbctemplateApplicationTests.java ================================================ package com.xkcoding.orm.jdbctemplate; 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 SpringBootDemoOrmJdbctemplateApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-orm-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-orm-jpa/README.md ================================================ # spring-boot-demo-orm-jpa > 此 demo 主要演示了 Spring Boot 如何使用 JPA 操作数据库,包含简单使用以及级联使用。 ## 主要代码 ### pom.xml ```xml 4.0.0 spring-boot-demo-orm-jpa 1.0.0-SNAPSHOT jar spring-boot-demo-orm-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-data-jpa org.springframework.boot spring-boot-starter mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true spring-boot-demo-orm-jpa org.springframework.boot spring-boot-maven-plugin ``` ### JpaConfig.java ```java /** *

    * JPA配置类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 11:05 */ @Configuration @EnableTransactionManagement @EnableJpaAuditing @EnableJpaRepositories(basePackages = "com.xkcoding.orm.jpa.repository", transactionManagerRef = "jpaTransactionManager") public class JpaConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { HibernateJpaVendorAdapter japVendor = new HibernateJpaVendorAdapter(); japVendor.setGenerateDdl(false); LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); entityManagerFactory.setDataSource(dataSource()); entityManagerFactory.setJpaVendorAdapter(japVendor); entityManagerFactory.setPackagesToScan("com.xkcoding.orm.jpa.entity"); return entityManagerFactory; } @Bean public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory); return transactionManager; } } ``` ### User.java ```java /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:06 */ @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor @Data @Builder @Entity @Table(name = "orm_user") @ToString(callSuper = true) public class User extends AbstractAuditModel { /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ @Column(name = "phone_number") private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 上次登录时间 */ @Column(name = "last_login_time") private Date lastLoginTime; /** * 关联部门表 * 1、关系维护端,负责多对多关系的绑定和解除 * 2、@JoinTable注解的name属性指定关联表的名字,joinColumns指定外键的名字,关联到关系维护端(User) * 3、inverseJoinColumns指定外键的名字,要关联的关系被维护端(Department) * 4、其实可以不使用@JoinTable注解,默认生成的关联表名称为主表表名+下划线+从表表名, * 即表名为user_department * 关联到主表的外键名:主表名+下划线+主表中的主键列名,即user_id,这里使用referencedColumnName指定 * 关联到从表的外键名:主表中用于关联的属性名+下划线+从表的主键列名,department_id * 主表就是关系维护端对应的表,从表就是关系被维护端对应的表 */ @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinTable(name = "orm_user_dept", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "dept_id", referencedColumnName = "id")) private Collection departmentList; } ``` ### Department.java ```java /** *

    * 部门实体类 *

    * * @author 76peter * @date Created in 2019-10-01 18:07 */ @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "orm_department") @ToString(callSuper = true) public class Department extends AbstractAuditModel { /** * 部门名 */ @Column(name = "name", columnDefinition = "varchar(255) not null") private String name; /** * 上级部门id */ @ManyToOne(cascade = {CascadeType.REFRESH}, optional = true) @JoinColumn(name = "superior", referencedColumnName = "id") private Department superior; /** * 所属层级 */ @Column(name = "levels", columnDefinition = "int not null default 0") private Integer levels; /** * 排序 */ @Column(name = "order_no", columnDefinition = "int not null default 0") private Integer orderNo; /** * 子部门集合 */ @OneToMany(cascade = {CascadeType.REFRESH, CascadeType.REMOVE}, fetch = FetchType.EAGER, mappedBy = "superior") private Collection children; /** * 部门下用户集合 */ @ManyToMany(mappedBy = "departmentList") private Collection userList; } ``` ### AbstractAuditModel.java ```java /** *

    * 实体通用父类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:01 */ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Data public abstract class AbstractAuditModel implements Serializable { /** * 主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 创建时间 */ @Temporal(TemporalType.TIMESTAMP) @Column(name = "create_time", nullable = false, updatable = false) @CreatedDate private Date createTime; /** * 上次更新时间 */ @Temporal(TemporalType.TIMESTAMP) @Column(name = "last_update_time", nullable = false) @LastModifiedDate private Date lastUpdateTime; } ``` ### UserDao.java ```java /** *

    * User Dao *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:07 */ @Repository public interface UserDao extends JpaRepository { } ``` ### DepartmentDao.java ```java /** *

    * User Dao *

    * * @author 76peter * @date Created in 2019-10-01 18:07 */ @Repository public interface DepartmentDao extends JpaRepository { /** * 根据层级查询部门 * * @param level 层级 * @return 部门列表 */ List findDepartmentsByLevels(Integer level); } ``` ### application.yml ```yaml server: port: 8080 servlet: context-path: /demo spring: datasource: jdbc-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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 jpa: show-sql: true hibernate: ddl-auto: validate properties: hibernate: dialect: org.hibernate.dialect.MySQL57InnoDBDialect open-in-view: true logging: level: com.xkcoding: debug org.hibernate.SQL: debug org.hibernate.type: trace ``` ### UserDaoTest.java ```java /** *

    * jpa 测试类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:09 */ @Slf4j public class UserDaoTest extends SpringBootDemoOrmJpaApplicationTests { @Autowired private UserDao userDao; /** * 测试保存 */ @Test public void testSave() { String salt = IdUtil.fastSimpleUUID(); User testSave3 = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).build(); userDao.save(testSave3); Assert.assertNotNull(testSave3.getId()); Optional byId = userDao.findById(testSave3.getId()); Assert.assertTrue(byId.isPresent()); log.debug("【byId】= {}", byId.get()); } /** * 测试删除 */ @Test public void testDelete() { long count = userDao.count(); userDao.deleteById(1L); long left = userDao.count(); Assert.assertEquals(count - 1, left); } /** * 测试修改 */ @Test public void testUpdate() { userDao.findById(1L).ifPresent(user -> { user.setName("JPA修改名字"); userDao.save(user); }); Assert.assertEquals("JPA修改名字", userDao.findById(1L).get().getName()); } /** * 测试查询单个 */ @Test public void testQueryOne() { Optional byId = userDao.findById(1L); Assert.assertTrue(byId.isPresent()); log.debug("【byId】= {}", byId.get()); } /** * 测试查询所有 */ @Test public void testQueryAll() { List users = userDao.findAll(); Assert.assertNotEquals(0, users.size()); log.debug("【users】= {}", users); } /** * 测试分页排序查询 */ @Test public void testQueryPage() { // 初始化数据 initData(); // JPA分页的时候起始页是页码减1 Integer currentPage = 0; Integer pageSize = 5; Sort sort = Sort.by(Sort.Direction.DESC, "id"); PageRequest pageRequest = PageRequest.of(currentPage, pageSize, sort); Page userPage = userDao.findAll(pageRequest); Assert.assertEquals(5, userPage.getSize()); Assert.assertEquals(userDao.count(), userPage.getTotalElements()); log.debug("【id】= {}", userPage.getContent().stream().map(User::getId).collect(Collectors.toList())); } /** * 初始化10条数据 */ private void initData() { List userList = Lists.newArrayList(); for (int i = 0; i < 10; i++) { String salt = IdUtil.fastSimpleUUID(); int index = 3 + i; User user = User.builder().name("testSave" + index).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + index + "@xkcoding.com").phoneNumber("1730000000" + index).status(1).lastLoginTime(new DateTime()).build(); userList.add(user); } userDao.saveAll(userList); } } ``` ### DepartmentDaoTest.java ```java /** *

    * jpa 测试类 *

    * * @author 76peter * @date Created in 2018-11-07 14:09 */ @Slf4j public class DepartmentDaoTest extends SpringBootDemoOrmJpaApplicationTests { @Autowired private DepartmentDao departmentDao; @Autowired private UserDao userDao; /** * 测试保存 ,根节点 */ @Test @Transactional public void testSave() { Collection departmentList = departmentDao.findDepartmentsByLevels(0); if (departmentList.size() == 0) { Department testSave1 = Department.builder().name("testSave1").orderNo(0).levels(0).superior(null).build(); Department testSave1_1 = Department.builder().name("testSave1_1").orderNo(0).levels(1).superior(testSave1).build(); Department testSave1_2 = Department.builder().name("testSave1_2").orderNo(0).levels(1).superior(testSave1).build(); Department testSave1_1_1 = Department.builder().name("testSave1_1_1").orderNo(0).levels(2).superior(testSave1_1).build(); departmentList.add(testSave1); departmentList.add(testSave1_1); departmentList.add(testSave1_2); departmentList.add(testSave1_1_1); departmentDao.saveAll(departmentList); Collection deptall = departmentDao.findAll(); log.debug("【部门】= {}", JSONArray.toJSONString((List) deptall)); } userDao.findById(1L).ifPresent(user -> { user.setName("添加部门"); Department dept = departmentDao.findById(2L).get(); user.setDepartmentList(departmentList); userDao.save(user); }); log.debug("用户部门={}", JSONUtil.toJsonStr(userDao.findById(1L).get().getDepartmentList())); departmentDao.findById(2L).ifPresent(dept -> { Collection userlist = dept.getUserList(); //关联关系由user维护中间表,department userlist不会发生变化,可以增加查询方法来处理 重写getUserList方法 log.debug("部门下用户={}", JSONUtil.toJsonStr(userlist)); }); userDao.findById(1L).ifPresent(user -> { user.setName("清空部门"); user.setDepartmentList(null); userDao.save(user); }); log.debug("用户部门={}", userDao.findById(1L).get().getDepartmentList()); } } ``` ### 其余代码及 SQL 参见本 demo ## 参考 - Spring Data JPA 官方文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/ ================================================ FILE: demo-orm-jpa/pom.xml ================================================ 4.0.0 demo-orm-jpa 1.0.0-SNAPSHOT jar demo-orm-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-data-jpa org.springframework.boot spring-boot-starter mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-orm-jpa org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/SpringBootDemoOrmJpaApplication.java ================================================ package com.xkcoding.orm.jpa; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-28 22:58 */ @SpringBootApplication public class SpringBootDemoOrmJpaApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmJpaApplication.class, args); } } ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/config/JpaConfig.java ================================================ package com.xkcoding.orm.jpa.config; 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 org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; /** *

    * JPA配置类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 11:05 */ @Configuration @EnableTransactionManagement @EnableJpaAuditing @EnableJpaRepositories(basePackages = "com.xkcoding.orm.jpa.repository", transactionManagerRef = "jpaTransactionManager") public class JpaConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { HibernateJpaVendorAdapter japVendor = new HibernateJpaVendorAdapter(); japVendor.setGenerateDdl(false); LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); entityManagerFactory.setDataSource(dataSource()); entityManagerFactory.setJpaVendorAdapter(japVendor); entityManagerFactory.setPackagesToScan("com.xkcoding.orm.jpa.entity"); return entityManagerFactory; } @Bean public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory); return transactionManager; } } ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/entity/Department.java ================================================ package com.xkcoding.orm.jpa.entity; import com.xkcoding.orm.jpa.entity.base.AbstractAuditModel; import lombok.*; import javax.persistence.*; import java.util.Collection; /** *

    * 部门实体类 *

    * * @author 76peter * @date Created in 2019-10-01 18:07 */ @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "orm_department") @ToString(callSuper = true) public class Department extends AbstractAuditModel { /** * 部门名 */ @Column(name = "name", columnDefinition = "varchar(255) not null") private String name; /** * 上级部门id */ @ManyToOne(cascade = {CascadeType.REFRESH}, optional = true) @JoinColumn(name = "superior", referencedColumnName = "id") private Department superior; /** * 所属层级 */ @Column(name = "levels", columnDefinition = "int not null default 0") private Integer levels; /** * 排序 */ @Column(name = "order_no", columnDefinition = "int not null default 0") private Integer orderNo; /** * 子部门集合 */ @OneToMany(cascade = {CascadeType.REFRESH, CascadeType.REMOVE}, fetch = FetchType.EAGER, mappedBy = "superior") private Collection children; /** * 部门下用户集合 */ @ManyToMany(mappedBy = "departmentList") private Collection userList; } ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/entity/User.java ================================================ package com.xkcoding.orm.jpa.entity; import com.xkcoding.orm.jpa.entity.base.AbstractAuditModel; import lombok.*; import javax.persistence.*; import java.util.Collection; import java.util.Date; /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:06 */ @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor @Data @Builder @Entity @Table(name = "orm_user") @ToString(callSuper = true) public class User extends AbstractAuditModel { /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ @Column(name = "phone_number") private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 上次登录时间 */ @Column(name = "last_login_time") private Date lastLoginTime; /** * 关联部门表 * 1、关系维护端,负责多对多关系的绑定和解除 * 2、@JoinTable注解的name属性指定关联表的名字,joinColumns指定外键的名字,关联到关系维护端(User) * 3、inverseJoinColumns指定外键的名字,要关联的关系被维护端(Department) * 4、其实可以不使用@JoinTable注解,默认生成的关联表名称为主表表名+下划线+从表表名, * 即表名为user_department * 关联到主表的外键名:主表名+下划线+主表中的主键列名,即user_id,这里使用referencedColumnName指定 * 关联到从表的外键名:主表中用于关联的属性名+下划线+从表的主键列名,department_id * 主表就是关系维护端对应的表,从表就是关系被维护端对应的表 */ @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinTable(name = "orm_user_dept", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "dept_id", referencedColumnName = "id")) private Collection departmentList; } ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/entity/base/AbstractAuditModel.java ================================================ package com.xkcoding.orm.jpa.entity.base; import lombok.Data; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.io.Serializable; import java.util.Date; /** *

    * 实体通用父类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:01 */ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Data public abstract class AbstractAuditModel implements Serializable { /** * 主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 创建时间 */ @Temporal(TemporalType.TIMESTAMP) @Column(name = "create_time", nullable = false, updatable = false) @CreatedDate private Date createTime; /** * 上次更新时间 */ @Temporal(TemporalType.TIMESTAMP) @Column(name = "last_update_time", nullable = false) @LastModifiedDate private Date lastUpdateTime; } ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/repository/DepartmentDao.java ================================================ package com.xkcoding.orm.jpa.repository; import com.xkcoding.orm.jpa.entity.Department; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; /** *

    * User Dao *

    * * @author 76peter * @date Created in 2019-10-01 18:07 */ @Repository public interface DepartmentDao extends JpaRepository { /** * 根据层级查询部门 * * @param level 层级 * @return 部门列表 */ List findDepartmentsByLevels(Integer level); } ================================================ FILE: demo-orm-jpa/src/main/java/com/xkcoding/orm/jpa/repository/UserDao.java ================================================ package com.xkcoding.orm.jpa.repository; import com.xkcoding.orm.jpa.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** *

    * User Dao *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:07 */ @Repository public interface UserDao extends JpaRepository { } ================================================ FILE: demo-orm-jpa/src/main/resources/application.yml ================================================ spring: datasource: jdbc-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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 jpa: show-sql: true hibernate: ddl-auto: validate 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-orm-jpa/src/main/resources/db/data.sql ================================================ INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (1, 'user_1', 'ff342e862e7c3285cdc07e56d6b8973b', '412365a109674b2dbb1981ed561a4c70', 'user1@xkcoding.com', '17300000001'); INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (2, 'user_2', '6c6bf02c8d5d3d128f34b1700cb1e32c', 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user2@xkcoding.com', '17300000002'); ================================================ FILE: demo-orm-jpa/src/main/resources/db/schema.sql ================================================ DROP TABLE IF EXISTS `orm_user`; CREATE TABLE `orm_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(32) NOT NULL COMMENT '加密后的密码', `salt` VARCHAR(32) NOT NULL COMMENT '加密使用的盐', `email` VARCHAR(32) NOT NULL UNIQUE COMMENT '邮箱', `phone_number` VARCHAR(15) NOT NULL UNIQUE COMMENT '手机号码', `status` INT(2) NOT NULL DEFAULT 1 COMMENT '状态,-1:逻辑删除,0:禁用,1:启用', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_login_time` DATETIME DEFAULT NULL COMMENT '上次登录时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; DROP TABLE IF EXISTS `orm_department`; CREATE TABLE `orm_department` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL COMMENT '部门名称', `superior` INT(11) COMMENT '上级id', `levels` INT(11) NOT NULL COMMENT '层级', `order_no` INT(11) NOT NULL DEFAULT 0 COMMENT '排序', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; DROP TABLE IF EXISTS `orm_user_dept`; CREATE TABLE `orm_user_dept` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `user_id` INT(11) NOT NULL COMMENT '用户id', `dept_id` INT(11) NOT NULL COMMENT '部门id', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; ================================================ FILE: demo-orm-jpa/src/test/java/com/xkcoding/orm/jpa/SpringBootDemoOrmJpaApplicationTests.java ================================================ package com.xkcoding.orm.jpa; 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 SpringBootDemoOrmJpaApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-orm-jpa/src/test/java/com/xkcoding/orm/jpa/repository/DepartmentDaoTest.java ================================================ package com.xkcoding.orm.jpa.repository; import cn.hutool.json.JSONUtil; import com.xkcoding.orm.jpa.SpringBootDemoOrmJpaApplicationTests; import com.xkcoding.orm.jpa.entity.Department; import com.xkcoding.orm.jpa.entity.User; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONArray; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import javax.transaction.Transactional; import java.util.Collection; import java.util.List; /** *

    * jpa 测试类 *

    * * @author 76peter * @date Created in 2018-11-07 14:09 */ @Slf4j public class DepartmentDaoTest extends SpringBootDemoOrmJpaApplicationTests { @Autowired private DepartmentDao departmentDao; @Autowired private UserDao userDao; /** * 测试保存 ,根节点 */ @Test @Transactional public void testSave() { Collection departmentList = departmentDao.findDepartmentsByLevels(0); if (departmentList.size() == 0) { Department testSave1 = Department.builder().name("testSave1").orderNo(0).levels(0).superior(null).build(); Department testSave1_1 = Department.builder().name("testSave1_1").orderNo(0).levels(1).superior(testSave1).build(); Department testSave1_2 = Department.builder().name("testSave1_2").orderNo(0).levels(1).superior(testSave1).build(); Department testSave1_1_1 = Department.builder().name("testSave1_1_1").orderNo(0).levels(2).superior(testSave1_1).build(); departmentList.add(testSave1); departmentList.add(testSave1_1); departmentList.add(testSave1_2); departmentList.add(testSave1_1_1); departmentDao.saveAll(departmentList); Collection deptall = departmentDao.findAll(); log.debug("【部门】= {}", JSONArray.toJSONString((List) deptall)); } userDao.findById(1L).ifPresent(user -> { user.setName("添加部门"); Department dept = departmentDao.findById(2L).get(); user.setDepartmentList(departmentList); userDao.save(user); }); log.debug("用户部门={}", JSONUtil.toJsonStr(userDao.findById(1L).get().getDepartmentList())); departmentDao.findById(2L).ifPresent(dept -> { Collection userlist = dept.getUserList(); //关联关系由user维护中间表,department userlist不会发生变化,可以增加查询方法来处理 重写getUserList方法 log.debug("部门下用户={}", JSONUtil.toJsonStr(userlist)); }); userDao.findById(1L).ifPresent(user -> { user.setName("清空部门"); user.setDepartmentList(null); userDao.save(user); }); log.debug("用户部门={}", userDao.findById(1L).get().getDepartmentList()); } } ================================================ FILE: demo-orm-jpa/src/test/java/com/xkcoding/orm/jpa/repository/UserDaoTest.java ================================================ package com.xkcoding.orm.jpa.repository; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.SecureUtil; import com.xkcoding.orm.jpa.SpringBootDemoOrmJpaApplicationTests; import com.xkcoding.orm.jpa.entity.User; import lombok.extern.slf4j.Slf4j; import org.assertj.core.util.Lists; import org.junit.Assert; 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 java.util.List; import java.util.Optional; import java.util.stream.Collectors; /** *

    * jpa 测试类 *

    * * @author yangkai.shen * @date Created in 2018-11-07 14:09 */ @Slf4j public class UserDaoTest extends SpringBootDemoOrmJpaApplicationTests { @Autowired private UserDao userDao; /** * 测试保存 */ @Test public void testSave() { String salt = IdUtil.fastSimpleUUID(); User testSave3 = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).build(); userDao.save(testSave3); Assert.assertNotNull(testSave3.getId()); Optional byId = userDao.findById(testSave3.getId()); Assert.assertTrue(byId.isPresent()); log.debug("【byId】= {}", byId.get()); } /** * 测试删除 */ @Test public void testDelete() { long count = userDao.count(); userDao.deleteById(1L); long left = userDao.count(); Assert.assertEquals(count - 1, left); } /** * 测试修改 */ @Test public void testUpdate() { userDao.findById(1L).ifPresent(user -> { user.setName("JPA修改名字"); userDao.save(user); }); Assert.assertEquals("JPA修改名字", userDao.findById(1L).get().getName()); } /** * 测试查询单个 */ @Test public void testQueryOne() { Optional byId = userDao.findById(1L); Assert.assertTrue(byId.isPresent()); log.debug("【byId】= {}", byId.get()); } /** * 测试查询所有 */ @Test public void testQueryAll() { List users = userDao.findAll(); Assert.assertNotEquals(0, users.size()); log.debug("【users】= {}", users); } /** * 测试分页排序查询 */ @Test public void testQueryPage() { // 初始化数据 initData(); // JPA分页的时候起始页是页码减1 Integer currentPage = 0; Integer pageSize = 5; Sort sort = Sort.by(Sort.Direction.DESC, "id"); PageRequest pageRequest = PageRequest.of(currentPage, pageSize, sort); Page userPage = userDao.findAll(pageRequest); Assert.assertEquals(5, userPage.getSize()); Assert.assertEquals(userDao.count(), userPage.getTotalElements()); log.debug("【id】= {}", userPage.getContent().stream().map(User::getId).collect(Collectors.toList())); } /** * 初始化10条数据 */ private void initData() { List userList = Lists.newArrayList(); for (int i = 0; i < 10; i++) { String salt = IdUtil.fastSimpleUUID(); int index = 3 + i; User user = User.builder().name("testSave" + index).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + index + "@xkcoding.com").phoneNumber("1730000000" + index).status(1).lastLoginTime(new DateTime()).build(); userList.add(user); } userDao.saveAll(userList); } } ================================================ FILE: demo-orm-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-orm-mybatis/README.md ================================================ # spring-boot-demo-orm-mybatis > 此 demo 演示了 Spring Boot 如何与原生的 mybatis 整合,使用了 mybatis 官方提供的脚手架 `mybatis-spring-boot-starter `可以很容易的和 Spring Boot 整合。 ## pom.xml ```xml 4.0.0 spring-boot-demo-orm-mybatis 1.0.0-SNAPSHOT jar spring-boot-demo-orm-mybatis Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.3.2 org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.version} mysql mysql-connector-java org.projectlombok lombok true cn.hutool hutool-all com.google.guava guava org.springframework.boot spring-boot-starter-test test spring-boot-demo-orm-mybatis org.springframework.boot spring-boot-maven-plugin ``` ## SpringBootDemoOrmMybatisApplication.java ```java /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 10:52 */ @MapperScan(basePackages = {"com.xkcoding.orm.mybatis.mapper"}) @SpringBootApplication public class SpringBootDemoOrmMybatisApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmMybatisApplication.class, args); } } ``` ## application.yml ```yaml spring: datasource: 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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.mybatis.mapper: trace mybatis: configuration: # 下划线转驼峰 map-underscore-to-camel-case: true mapper-locations: classpath:mappers/*.xml type-aliases-package: com.xkcoding.orm.mybatis.entity ``` ## UserMapper.java ```java /** *

    * User Mapper *

    * * @author yangkai.shen * @date Created in 2018-11-08 10:54 */ @Mapper @Component public interface UserMapper { /** * 查询所有用户 * * @return 用户列表 */ @Select("SELECT * FROM orm_user") List selectAllUser(); /** * 根据id查询用户 * * @param id 主键id * @return 当前id的用户,不存在则是 {@code null} */ @Select("SELECT * FROM orm_user WHERE id = #{id}") User selectUserById(@Param("id") Long id); /** * 保存用户 * * @param user 用户 * @return 成功 - {@code 1} 失败 - {@code 0} */ int saveUser(@Param("user") User user); /** * 删除用户 * * @param id 主键id * @return 成功 - {@code 1} 失败 - {@code 0} */ int deleteById(@Param("id") Long id); } ``` ## UserMapper.xml ```xml INSERT INTO `orm_user` (`name`, `password`, `salt`, `email`, `phone_number`, `status`, `create_time`, `last_login_time`, `last_update_time`) VALUES (#{user.name}, #{user.password}, #{user.salt}, #{user.email}, #{user.phoneNumber}, #{user.status}, #{user.createTime}, #{user.lastLoginTime}, #{user.lastUpdateTime}) DELETE FROM `orm_user` WHERE `id` = #{id} ``` ## UserMapperTest.java ```java /** *

    * UserMapper 测试类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 11:25 */ @Slf4j public class UserMapperTest extends SpringBootDemoOrmMybatisApplicationTests { @Autowired private UserMapper userMapper; @Test public void selectAllUser() { List userList = userMapper.selectAllUser(); Assert.assertTrue(CollUtil.isNotEmpty(userList)); log.debug("【userList】= {}", userList); } @Test public void selectUserById() { User user = userMapper.selectUserById(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } @Test public void saveUser() { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); int i = userMapper.saveUser(user); Assert.assertEquals(1, i); } @Test public void deleteById() { int i = userMapper.deleteById(1L); Assert.assertEquals(1, i); } } ``` ## 参考 - Mybatis官方文档:http://www.mybatis.org/mybatis-3/zh/index.html - Mybatis官方脚手架文档:http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/ - Mybatis整合Spring Boot官方demo:https://github.com/mybatis/spring-boot-starter/tree/master/mybatis-spring-boot-samples ================================================ FILE: demo-orm-mybatis/pom.xml ================================================ 4.0.0 demo-orm-mybatis 1.0.0-SNAPSHOT jar demo-orm-mybatis Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.3.2 org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.version} mysql mysql-connector-java org.projectlombok lombok true cn.hutool hutool-all com.google.guava guava org.springframework.boot spring-boot-starter-test test demo-orm-mybatis org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-mybatis/src/main/java/com/xkcoding/orm/mybatis/SpringBootDemoOrmMybatisApplication.java ================================================ package com.xkcoding.orm.mybatis; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 10:52 */ @MapperScan(basePackages = {"com.xkcoding.orm.mybatis.mapper"}) @SpringBootApplication public class SpringBootDemoOrmMybatisApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmMybatisApplication.class, args); } } ================================================ FILE: demo-orm-mybatis/src/main/java/com/xkcoding/orm/mybatis/entity/User.java ================================================ package com.xkcoding.orm.mybatis.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 10:58 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class User implements Serializable { private static final long serialVersionUID = -1840831686851699943L; /** * 主键 */ private Long id; /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 创建时间 */ private Date createTime; /** * 上次登录时间 */ private Date lastLoginTime; /** * 上次更新时间 */ private Date lastUpdateTime; } ================================================ FILE: demo-orm-mybatis/src/main/java/com/xkcoding/orm/mybatis/mapper/UserMapper.java ================================================ package com.xkcoding.orm.mybatis.mapper; import com.xkcoding.orm.mybatis.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Component; import java.util.List; /** *

    * User Mapper *

    * * @author yangkai.shen * @date Created in 2018-11-08 10:54 */ @Mapper @Component public interface UserMapper { /** * 查询所有用户 * * @return 用户列表 */ @Select("SELECT * FROM orm_user") List selectAllUser(); /** * 根据id查询用户 * * @param id 主键id * @return 当前id的用户,不存在则是 {@code null} */ @Select("SELECT * FROM orm_user WHERE id = #{id}") User selectUserById(@Param("id") Long id); /** * 保存用户 * * @param user 用户 * @return 成功 - {@code 1} 失败 - {@code 0} */ int saveUser(@Param("user") User user); /** * 删除用户 * * @param id 主键id * @return 成功 - {@code 1} 失败 - {@code 0} */ int deleteById(@Param("id") Long id); } ================================================ FILE: demo-orm-mybatis/src/main/resources/application.yml ================================================ spring: datasource: 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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.mybatis.mapper: trace mybatis: configuration: # 下划线转驼峰 map-underscore-to-camel-case: true mapper-locations: classpath:mappers/*.xml type-aliases-package: com.xkcoding.orm.mybatis.entity ================================================ FILE: demo-orm-mybatis/src/main/resources/db/data.sql ================================================ INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (1, 'user_1', 'ff342e862e7c3285cdc07e56d6b8973b', '412365a109674b2dbb1981ed561a4c70', 'user1@xkcoding.com', '17300000001'); INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (2, 'user_2', '6c6bf02c8d5d3d128f34b1700cb1e32c', 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user2@xkcoding.com', '17300000002'); ================================================ FILE: demo-orm-mybatis/src/main/resources/db/schema.sql ================================================ DROP TABLE IF EXISTS `orm_user`; CREATE TABLE `orm_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(32) NOT NULL COMMENT '加密后的密码', `salt` VARCHAR(32) NOT NULL COMMENT '加密使用的盐', `email` VARCHAR(32) NOT NULL UNIQUE COMMENT '邮箱', `phone_number` VARCHAR(15) NOT NULL UNIQUE COMMENT '手机号码', `status` INT(2) NOT NULL DEFAULT 1 COMMENT '状态,-1:逻辑删除,0:禁用,1:启用', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_login_time` DATETIME DEFAULT NULL COMMENT '上次登录时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; ================================================ FILE: demo-orm-mybatis/src/main/resources/mappers/UserMapper.xml ================================================ INSERT INTO `orm_user` (`name`, `password`, `salt`, `email`, `phone_number`, `status`, `create_time`, `last_login_time`, `last_update_time`) VALUES (#{user.name}, #{user.password}, #{user.salt}, #{user.email}, #{user.phoneNumber}, #{user.status}, #{user.createTime}, #{user.lastLoginTime}, #{user.lastUpdateTime}) DELETE FROM `orm_user` WHERE `id` = #{id} ================================================ FILE: demo-orm-mybatis/src/test/java/com/xkcoding/orm/mybatis/SpringBootDemoOrmMybatisApplicationTests.java ================================================ package com.xkcoding.orm.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 SpringBootDemoOrmMybatisApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-orm-mybatis/src/test/java/com/xkcoding/orm/mybatis/mapper/UserMapperTest.java ================================================ package com.xkcoding.orm.mybatis.mapper; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.SecureUtil; import com.xkcoding.orm.mybatis.SpringBootDemoOrmMybatisApplicationTests; import com.xkcoding.orm.mybatis.entity.User; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** *

    * UserMapper 测试类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 11:25 */ @Slf4j public class UserMapperTest extends SpringBootDemoOrmMybatisApplicationTests { @Autowired private UserMapper userMapper; /** * 测试查询所有 */ @Test public void selectAllUser() { List userList = userMapper.selectAllUser(); Assert.assertTrue(CollUtil.isNotEmpty(userList)); log.debug("【userList】= {}", userList); } /** * 测试根据主键查询单个 */ @Test public void selectUserById() { User user = userMapper.selectUserById(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } /** * 测试保存 */ @Test public void saveUser() { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); int i = userMapper.saveUser(user); Assert.assertEquals(1, i); } /** * 测试根据主键删除 */ @Test public void deleteById() { int i = userMapper.deleteById(1L); Assert.assertEquals(1, i); } } ================================================ FILE: demo-orm-mybatis-mapper-page/.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-orm-mybatis-mapper-page/README.md ================================================ # spring-boot-demo-orm-mybatis-mapper-page > 此 demo 演示了 Spring Boot 如何集成通用Mapper插件和分页助手插件,简化Mybatis开发,带给你难以置信的开发体验。 ## pom.xml ```xml 4.0.0 spring-boot-demo-orm-mybatis-mapper-page 1.0.0-SNAPSHOT jar spring-boot-demo-orm-mybatis-mapper-page Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.0.4 1.2.9 org.springframework.boot spring-boot-starter tk.mybatis mapper-spring-boot-starter ${mybatis.mapper.version} com.github.pagehelper pagehelper-spring-boot-starter ${mybatis.pagehelper.version} org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all com.google.guava guava mysql mysql-connector-java spring-boot-demo-orm-mybatis-mapper-page org.springframework.boot spring-boot-maven-plugin ``` ## SpringBootDemoOrmMybatisApplication.java ```java /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-08 13:43 */ @SpringBootApplication @MapperScan(basePackages = {"com.xkcoding.orm.mybatis.MapperAndPage.mapper"}) // 注意:这里的 MapperScan 是 tk.mybatis.spring.annotation.MapperScan 这个包下的 public class SpringBootDemoOrmMybatisMapperPageApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmMybatisMapperPageApplication.class, args); } } ``` ## application.yml ```yaml spring: datasource: 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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.mybatis.MapperAndPage.mapper: trace mybatis: configuration: # 下划线转驼峰 map-underscore-to-camel-case: true mapper-locations: classpath:mappers/*.xml type-aliases-package: com.xkcoding.orm.mybatis.MapperAndPage.entity mapper: mappers: - tk.mybatis.mapper.common.Mapper not-empty: true style: camelhump wrap-keyword: "`{0}`" safe-delete: true safe-update: true identity: MYSQL pagehelper: auto-dialect: true helper-dialect: mysql reasonable: true params: count=countSql ``` ## UserMapper.java ```java /** *

    * UserMapper *

    * * @author yangkai.shen * @date Created in 2018-11-08 14:15 */ @Component // 注意:这里的Mapper是tk.mybatis.mapper.common.Mapper包下的 public interface UserMapper extends Mapper, MySqlMapper { } ``` ## UserMapperTest.java ```java /** *

    * UserMapper 测试 *

    * * @author yangkai.shen * @date Created in 2018-11-08 14:25 */ @Slf4j public class UserMapperTest extends SpringBootDemoOrmMybatisMapperPageApplicationTests { @Autowired private UserMapper userMapper; /** * 测试通用Mapper - 保存 */ @Test public void testInsert() { String salt = IdUtil.fastSimpleUUID(); User testSave3 = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); userMapper.insertUseGeneratedKeys(testSave3); Assert.assertNotNull(testSave3.getId()); log.debug("【测试主键回写#testSave3.getId()】= {}", testSave3.getId()); } /** * 测试通用Mapper - 批量保存 */ @Test public void testInsertList() { List userList = Lists.newArrayList(); for (int i = 4; i < 14; i++) { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave" + i).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + i + "@xkcoding.com").phoneNumber("1730000000" + i).status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); userList.add(user); } int i = userMapper.insertList(userList); Assert.assertEquals(userList.size(), i); List ids = userList.stream().map(User::getId).collect(Collectors.toList()); log.debug("【测试主键回写#userList.ids】= {}", ids); } /** * 测试通用Mapper - 删除 */ @Test public void testDelete() { Long primaryKey = 1L; int i = userMapper.deleteByPrimaryKey(primaryKey); Assert.assertEquals(1, i); User user = userMapper.selectByPrimaryKey(primaryKey); Assert.assertNull(user); } /** * 测试通用Mapper - 更新 */ @Test public void testUpdate() { Long primaryKey = 1L; User user = userMapper.selectByPrimaryKey(primaryKey); user.setName("通用Mapper名字更新"); int i = userMapper.updateByPrimaryKeySelective(user); Assert.assertEquals(1, i); User update = userMapper.selectByPrimaryKey(primaryKey); Assert.assertNotNull(update); Assert.assertEquals("通用Mapper名字更新", update.getName()); log.debug("【update】= {}", update); } /** * 测试通用Mapper - 查询单个 */ @Test public void testQueryOne(){ User user = userMapper.selectByPrimaryKey(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } /** * 测试通用Mapper - 查询全部 */ @Test public void testQueryAll() { List users = userMapper.selectAll(); Assert.assertTrue(CollUtil.isNotEmpty(users)); log.debug("【users】= {}", users); } /** * 测试分页助手 - 分页排序查询 */ @Test public void testQueryByPageAndSort() { initData(); int currentPage = 1; int pageSize = 5; String orderBy = "id desc"; int count = userMapper.selectCount(null); PageHelper.startPage(currentPage, pageSize, orderBy); List users = userMapper.selectAll(); PageInfo userPageInfo = new PageInfo<>(users); Assert.assertEquals(5, userPageInfo.getSize()); Assert.assertEquals(count, userPageInfo.getTotal()); log.debug("【userPageInfo】= {}", userPageInfo); } /** * 测试通用Mapper - 条件查询 */ @Test public void testQueryByCondition() { initData(); Example example = new Example(User.class); // 过滤 example.createCriteria().andLike("name", "%Save1%").orEqualTo("phoneNumber", "17300000001"); // 排序 example.setOrderByClause("id desc"); int count = userMapper.selectCountByExample(example); // 分页 PageHelper.startPage(1, 3); // 查询 List userList = userMapper.selectByExample(example); PageInfo userPageInfo = new PageInfo<>(userList); Assert.assertEquals(3, userPageInfo.getSize()); Assert.assertEquals(count, userPageInfo.getTotal()); log.debug("【userPageInfo】= {}", userPageInfo); } /** * 初始化数据 */ private void initData() { testInsertList(); } } ``` ## 参考 - 通用Mapper官方文档:https://github.com/abel533/Mapper/wiki/1.integration - pagehelper 官方文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md ================================================ FILE: demo-orm-mybatis-mapper-page/pom.xml ================================================ 4.0.0 demo-orm-mybatis-mapper-page 1.0.0-SNAPSHOT jar demo-orm-mybatis-mapper-page Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.0.4 1.2.9 org.springframework.boot spring-boot-starter tk.mybatis mapper-spring-boot-starter ${mybatis.mapper.version} com.github.pagehelper pagehelper-spring-boot-starter ${mybatis.pagehelper.version} org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all com.google.guava guava mysql mysql-connector-java demo-orm-mybatis-mapper-page org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-mybatis-mapper-page/src/main/java/com/xkcoding/orm/mybatis/MapperAndPage/SpringBootDemoOrmMybatisMapperPageApplication.java ================================================ package com.xkcoding.orm.mybatis.MapperAndPage; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tk.mybatis.spring.annotation.MapperScan; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-08 13:43 */ @SpringBootApplication @MapperScan(basePackages = {"com.xkcoding.orm.mybatis.MapperAndPage.mapper"}) public class SpringBootDemoOrmMybatisMapperPageApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmMybatisMapperPageApplication.class, args); } } ================================================ FILE: demo-orm-mybatis-mapper-page/src/main/java/com/xkcoding/orm/mybatis/MapperAndPage/entity/User.java ================================================ package com.xkcoding.orm.mybatis.MapperAndPage.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import tk.mybatis.mapper.annotation.KeySql; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; import java.util.Date; /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 14:14 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @Table(name = "orm_user") public class User implements Serializable { private static final long serialVersionUID = -1840831686851699943L; /** * 主键 */ @Id @KeySql(useGeneratedKeys = true) private Long id; /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 创建时间 */ private Date createTime; /** * 上次登录时间 */ private Date lastLoginTime; /** * 上次更新时间 */ private Date lastUpdateTime; } ================================================ FILE: demo-orm-mybatis-mapper-page/src/main/java/com/xkcoding/orm/mybatis/MapperAndPage/mapper/UserMapper.java ================================================ package com.xkcoding.orm.mybatis.MapperAndPage.mapper; import com.xkcoding.orm.mybatis.MapperAndPage.entity.User; import org.springframework.stereotype.Component; import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; /** *

    * UserMapper *

    * * @author yangkai.shen * @date Created in 2018-11-08 14:15 */ @Component public interface UserMapper extends Mapper, MySqlMapper { } ================================================ FILE: demo-orm-mybatis-mapper-page/src/main/resources/application.yml ================================================ spring: datasource: 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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.mybatis.MapperAndPage.mapper: trace mybatis: configuration: # 下划线转驼峰 map-underscore-to-camel-case: true mapper-locations: classpath:mappers/*.xml type-aliases-package: com.xkcoding.orm.mybatis.MapperAndPage.entity mapper: mappers: - tk.mybatis.mapper.common.Mapper not-empty: true style: camelhump wrap-keyword: "`{0}`" safe-delete: true safe-update: true identity: MYSQL pagehelper: auto-dialect: true helper-dialect: mysql reasonable: true params: count=countSql ================================================ FILE: demo-orm-mybatis-mapper-page/src/main/resources/db/data.sql ================================================ INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (1, 'user_1', 'ff342e862e7c3285cdc07e56d6b8973b', '412365a109674b2dbb1981ed561a4c70', 'user1@xkcoding.com', '17300000001'); INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (2, 'user_2', '6c6bf02c8d5d3d128f34b1700cb1e32c', 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user2@xkcoding.com', '17300000002'); ================================================ FILE: demo-orm-mybatis-mapper-page/src/main/resources/db/schema.sql ================================================ DROP TABLE IF EXISTS `orm_user`; CREATE TABLE `orm_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(32) NOT NULL COMMENT '加密后的密码', `salt` VARCHAR(32) NOT NULL COMMENT '加密使用的盐', `email` VARCHAR(32) NOT NULL UNIQUE COMMENT '邮箱', `phone_number` VARCHAR(15) NOT NULL UNIQUE COMMENT '手机号码', `status` INT(2) NOT NULL DEFAULT 1 COMMENT '状态,-1:逻辑删除,0:禁用,1:启用', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_login_time` DATETIME DEFAULT NULL COMMENT '上次登录时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; ================================================ FILE: demo-orm-mybatis-mapper-page/src/test/java/com/xkcoding/orm/mybatis/MapperAndPage/SpringBootDemoOrmMybatisMapperPageApplicationTests.java ================================================ package com.xkcoding.orm.mybatis.MapperAndPage; 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 SpringBootDemoOrmMybatisMapperPageApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-orm-mybatis-mapper-page/src/test/java/com/xkcoding/orm/mybatis/MapperAndPage/mapper/UserMapperTest.java ================================================ package com.xkcoding.orm.mybatis.MapperAndPage.mapper; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.SecureUtil; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.xkcoding.orm.mybatis.MapperAndPage.SpringBootDemoOrmMybatisMapperPageApplicationTests; import com.xkcoding.orm.mybatis.MapperAndPage.entity.User; import lombok.extern.slf4j.Slf4j; import org.assertj.core.util.Lists; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import tk.mybatis.mapper.entity.Example; import java.util.List; import java.util.stream.Collectors; /** *

    * UserMapper 测试 *

    * * @author yangkai.shen * @date Created in 2018-11-08 14:25 */ @Slf4j public class UserMapperTest extends SpringBootDemoOrmMybatisMapperPageApplicationTests { @Autowired private UserMapper userMapper; /** * 测试通用Mapper - 保存 */ @Test public void testInsert() { String salt = IdUtil.fastSimpleUUID(); User testSave3 = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); userMapper.insertUseGeneratedKeys(testSave3); Assert.assertNotNull(testSave3.getId()); log.debug("【测试主键回写#testSave3.getId()】= {}", testSave3.getId()); } /** * 测试通用Mapper - 批量保存 */ @Test public void testInsertList() { List userList = Lists.newArrayList(); for (int i = 4; i < 14; i++) { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave" + i).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + i + "@xkcoding.com").phoneNumber("1730000000" + i).status(1).lastLoginTime(new DateTime()).createTime(new DateTime()).lastUpdateTime(new DateTime()).build(); userList.add(user); } int i = userMapper.insertList(userList); Assert.assertEquals(userList.size(), i); List ids = userList.stream().map(User::getId).collect(Collectors.toList()); log.debug("【测试主键回写#userList.ids】= {}", ids); } /** * 测试通用Mapper - 删除 */ @Test public void testDelete() { Long primaryKey = 1L; int i = userMapper.deleteByPrimaryKey(primaryKey); Assert.assertEquals(1, i); User user = userMapper.selectByPrimaryKey(primaryKey); Assert.assertNull(user); } /** * 测试通用Mapper - 更新 */ @Test public void testUpdate() { Long primaryKey = 1L; User user = userMapper.selectByPrimaryKey(primaryKey); user.setName("通用Mapper名字更新"); int i = userMapper.updateByPrimaryKeySelective(user); Assert.assertEquals(1, i); User update = userMapper.selectByPrimaryKey(primaryKey); Assert.assertNotNull(update); Assert.assertEquals("通用Mapper名字更新", update.getName()); log.debug("【update】= {}", update); } /** * 测试通用Mapper - 查询单个 */ @Test public void testQueryOne() { User user = userMapper.selectByPrimaryKey(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } /** * 测试通用Mapper - 查询全部 */ @Test public void testQueryAll() { List users = userMapper.selectAll(); Assert.assertTrue(CollUtil.isNotEmpty(users)); log.debug("【users】= {}", users); } /** * 测试分页助手 - 分页排序查询 */ @Test public void testQueryByPageAndSort() { initData(); int currentPage = 1; int pageSize = 5; String orderBy = "id desc"; int count = userMapper.selectCount(null); PageHelper.startPage(currentPage, pageSize, orderBy); List users = userMapper.selectAll(); PageInfo userPageInfo = new PageInfo<>(users); Assert.assertEquals(5, userPageInfo.getSize()); Assert.assertEquals(count, userPageInfo.getTotal()); log.debug("【userPageInfo】= {}", userPageInfo); } /** * 测试通用Mapper - 条件查询 */ @Test public void testQueryByCondition() { initData(); Example example = new Example(User.class); // 过滤 example.createCriteria().andLike("name", "%Save1%").orEqualTo("phoneNumber", "17300000001"); // 排序 example.setOrderByClause("id desc"); int count = userMapper.selectCountByExample(example); // 分页 PageHelper.startPage(1, 3); // 查询 List userList = userMapper.selectByExample(example); PageInfo userPageInfo = new PageInfo<>(userList); Assert.assertEquals(3, userPageInfo.getSize()); Assert.assertEquals(count, userPageInfo.getTotal()); log.debug("【userPageInfo】= {}", userPageInfo); } /** * 初始化数据 */ private void initData() { testInsertList(); } } ================================================ FILE: demo-orm-mybatis-plus/.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-orm-mybatis-plus/README.md ================================================ # spring-boot-demo-orm-mybatis-plus > 此 demo 演示了 Spring Boot 如何集成 mybatis-plus,简化Mybatis开发,带给你难以置信的开发体验。 > > - 2019-09-14 新增:ActiveRecord 模式操作 ## pom.xml ```xml 4.0.0 spring-boot-demo-orm-mybatis-plus 1.0.0-SNAPSHOT jar spring-boot-demo-orm-mybatis-plus Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 3.0.5 org.springframework.boot spring-boot-starter com.baomidou mybatis-plus-boot-starter ${mybatis.plus.version} org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true spring-boot-demo-orm-mybatis-plus org.springframework.boot spring-boot-maven-plugin ``` ## MybatisPlusConfig.java ```java /** *

    * mybatis-plus 配置 *

    * * @author yangkai.shen * @date Created in 2018-11-08 17:29 */ @Configuration @MapperScan(basePackages = {"com.xkcoding.orm.mybatis.plus.mapper"}) @EnableTransactionManagement public class MybatisPlusConfig { /** * 性能分析拦截器,不建议生产使用 */ @Bean public PerformanceInterceptor performanceInterceptor(){ return new PerformanceInterceptor(); } /** * 分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } } ``` ## CommonFieldHandler.java ```java package com.xkcoding.orm.mybatis.plus.config; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; /** *

    * 通用字段填充 *

    * * @author yangkai.shen * @date Created in 2018-11-08 17:40 */ @Slf4j @Component public class CommonFieldHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("lastUpdateTime", new Date(), metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("lastUpdateTime", new Date(), metaObject); } } ``` ## application.yml ```yaml spring: datasource: 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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.mybatis.plus.mapper: trace mybatis-plus: mapper-locations: classpath:mappers/*.xml #实体扫描,多个package用逗号或者分号分隔 typeAliasesPackage: com.xkcoding.orm.mybatis.plus.entity global-config: # 数据库相关配置 db-config: #主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID"; id-type: auto #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断" field-strategy: not_empty #驼峰下划线转换 table-underline: true #是否开启大写命名,默认不开启 #capital-mode: true #逻辑删除配置 #logic-delete-value: 1 #logic-not-delete-value: 0 db-type: mysql #刷新mapper 调试神器 refresh: true # 原生配置 configuration: map-underscore-to-camel-case: true cache-enabled: true ``` ## UserMapper.java ```java /** *

    * UserMapper *

    * * @author yangkai.shen * @date Created in 2018-11-08 16:57 */ @Component public interface UserMapper extends BaseMapper { } ``` ## UserService.java ```java /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-08 18:10 */ public interface UserService extends IService { } ``` ## UserServiceImpl.java ```java /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-08 18:10 */ @Service public class UserServiceImpl extends ServiceImpl implements UserService { } ``` ## UserServiceTest.java ```java /** *

    * User Service 测试 *

    * * @author yangkai.shen * @date Created in 2018-11-08 18:13 */ @Slf4j public class UserServiceTest extends SpringBootDemoOrmMybatisPlusApplicationTests { @Autowired private UserService userService; /** * 测试Mybatis-Plus 新增 */ @Test public void testSave() { String salt = IdUtil.fastSimpleUUID(); User testSave3 = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).build(); boolean save = userService.save(testSave3); Assert.assertTrue(save); log.debug("【测试id回显#testSave3.getId()】= {}", testSave3.getId()); } /** * 测试Mybatis-Plus 批量新增 */ @Test public void testSaveList() { List userList = Lists.newArrayList(); for (int i = 4; i < 14; i++) { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave" + i).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + i + "@xkcoding.com").phoneNumber("1730000000" + i).status(1).lastLoginTime(new DateTime()).build(); userList.add(user); } boolean batch = userService.saveBatch(userList); Assert.assertTrue(batch); List ids = userList.stream().map(User::getId).collect(Collectors.toList()); log.debug("【userList#ids】= {}", ids); } /** * 测试Mybatis-Plus 删除 */ @Test public void testDelete() { boolean remove = userService.removeById(1L); Assert.assertTrue(remove); User byId = userService.getById(1L); Assert.assertNull(byId); } /** * 测试Mybatis-Plus 修改 */ @Test public void testUpdate() { User user = userService.getById(1L); Assert.assertNotNull(user); user.setName("MybatisPlus修改名字"); boolean b = userService.updateById(user); Assert.assertTrue(b); User update = userService.getById(1L); Assert.assertEquals("MybatisPlus修改名字", update.getName()); log.debug("【update】= {}", update); } /** * 测试Mybatis-Plus 查询单个 */ @Test public void testQueryOne() { User user = userService.getById(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } /** * 测试Mybatis-Plus 查询全部 */ @Test public void testQueryAll() { List list = userService.list(new QueryWrapper<>()); Assert.assertTrue(CollUtil.isNotEmpty(list)); log.debug("【list】= {}", list); } /** * 测试Mybatis-Plus 分页排序查询 */ @Test public void testQueryByPageAndSort() { initData(); int count = userService.count(new QueryWrapper<>()); Page userPage = new Page<>(1, 5); userPage.setDesc("id"); IPage page = userService.page(userPage, new QueryWrapper<>()); Assert.assertEquals(5, page.getSize()); Assert.assertEquals(count, page.getTotal()); log.debug("【page.getRecords()】= {}", page.getRecords()); } /** * 测试Mybatis-Plus 自定义查询 */ @Test public void testQueryByCondition() { initData(); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "Save1").or().eq("phone_number", "17300000001").orderByDesc("id"); int count = userService.count(wrapper); Page userPage = new Page<>(1, 3); IPage page = userService.page(userPage, wrapper); Assert.assertEquals(3, page.getSize()); Assert.assertEquals(count, page.getTotal()); log.debug("【page.getRecords()】= {}", page.getRecords()); } /** * 初始化数据 */ private void initData() { testSaveList(); } } ``` ## 2019-09-14新增 ### ActiveRecord 模式 - Role.java ```java /** *

    * 角色实体类 *

    * * @author yangkai.shen * @date Created in 2019-09-16 14:04 */ @Data @TableName("orm_role") @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class Role extends Model { /** * 主键 */ private Long id; /** * 角色名 */ private String name; /** * 主键值,ActiveRecord 模式这个必须有,否则 xxById 的方法都将失效! * 即使使用 ActiveRecord 不会用到 RoleMapper,RoleMapper 这个接口也必须创建 */ @Override protected Serializable pkVal() { return this.id; } } ``` - RoleMapper.java ```java /** *

    * RoleMapper *

    * * @author yangkai.shen * @date Created in 2019-09-16 14:06 */ public interface RoleMapper extends BaseMapper { } ``` - ActiveRecordTest.java ```java /** *

    * Role *

    * * @author yangkai.shen * @date Created in 2019-09-16 14:19 */ @Slf4j public class ActiveRecordTest extends SpringBootDemoOrmMybatisPlusApplicationTests { /** * 测试 ActiveRecord 插入数据 */ @Test public void testActiveRecordInsert() { Role role = new Role(); role.setName("VIP"); Assert.assertTrue(role.insert()); // 成功直接拿会写的 ID log.debug("【role】= {}", role); } /** * 测试 ActiveRecord 更新数据 */ @Test public void testActiveRecordUpdate() { Assert.assertTrue(new Role().setId(1L).setName("管理员-1").updateById()); Assert.assertTrue(new Role().update(new UpdateWrapper().lambda().set(Role::getName, "普通用户-1").eq(Role::getId, 2))); } /** * 测试 ActiveRecord 查询数据 */ @Test public void testActiveRecordSelect() { Assert.assertEquals("管理员", new Role().setId(1L).selectById().getName()); Role role = new Role().selectOne(new QueryWrapper().lambda().eq(Role::getId, 2)); Assert.assertEquals("普通用户", role.getName()); List roles = new Role().selectAll(); Assert.assertTrue(roles.size() > 0); log.debug("【roles】= {}", roles); } /** * 测试 ActiveRecord 删除数据 */ @Test public void testActiveRecordDelete() { Assert.assertTrue(new Role().setId(1L).deleteById()); Assert.assertTrue(new Role().delete(new QueryWrapper().lambda().eq(Role::getName, "普通用户"))); } } ``` ## 参考 - mybatis-plus官方文档:http://mp.baomidou.com/ ================================================ FILE: demo-orm-mybatis-plus/pom.xml ================================================ 4.0.0 demo-orm-mybatis-plus 1.0.0-SNAPSHOT jar demo-orm-mybatis-plus Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 3.1.0 org.springframework.boot spring-boot-starter com.baomidou mybatis-plus-boot-starter ${mybatis.plus.version} org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-orm-mybatis-plus org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/SpringBootDemoOrmMybatisPlusApplication.java ================================================ package com.xkcoding.orm.mybatis.plus; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-08 16:48 */ @SpringBootApplication public class SpringBootDemoOrmMybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoOrmMybatisPlusApplication.class, args); } } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/config/CommonFieldHandler.java ================================================ package com.xkcoding.orm.mybatis.plus.config; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; /** *

    * 通用字段填充 *

    * * @author yangkai.shen * @date Created in 2018-11-08 17:40 */ @Slf4j @Component public class CommonFieldHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("lastUpdateTime", new Date(), metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("lastUpdateTime", new Date(), metaObject); } } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/config/MybatisPlusConfig.java ================================================ package com.xkcoding.orm.mybatis.plus.config; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** *

    * mybatis-plus 配置 *

    * * @author yangkai.shen * @date Created in 2018-11-08 17:29 */ @Configuration @MapperScan(basePackages = {"com.xkcoding.orm.mybatis.plus.mapper"}) @EnableTransactionManagement public class MybatisPlusConfig { /** * 性能分析拦截器,不建议生产使用 */ @Bean public PerformanceInterceptor performanceInterceptor() { return new PerformanceInterceptor(); } /** * 分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/entity/Role.java ================================================ package com.xkcoding.orm.mybatis.plus.entity; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.activerecord.Model; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.io.Serializable; /** *

    * 角色实体类 *

    * * @author yangkai.shen * @date Created in 2019-09-14 14:04 */ @Data @TableName("orm_role") @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class Role extends Model { /** * 主键 */ private Long id; /** * 角色名 */ private String name; /** * 主键值,ActiveRecord 模式这个必须有,否则 xxById 的方法都将失效! * 即使使用 ActiveRecord 不会用到 RoleMapper,RoleMapper 这个接口也必须创建 */ @Override protected Serializable pkVal() { return this.id; } } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/entity/User.java ================================================ package com.xkcoding.orm.mybatis.plus.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; import static com.baomidou.mybatisplus.annotation.FieldFill.INSERT; import static com.baomidou.mybatisplus.annotation.FieldFill.INSERT_UPDATE; /** *

    * 用户实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-08 16:49 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @TableName("orm_user") public class User implements Serializable { private static final long serialVersionUID = -1840831686851699943L; /** * 主键 */ private Long id; /** * 用户名 */ private String name; /** * 加密后的密码 */ private String password; /** * 加密使用的盐 */ private String salt; /** * 邮箱 */ private String email; /** * 手机号码 */ private String phoneNumber; /** * 状态,-1:逻辑删除,0:禁用,1:启用 */ private Integer status; /** * 创建时间 */ @TableField(fill = INSERT) private Date createTime; /** * 上次登录时间 */ private Date lastLoginTime; /** * 上次更新时间 */ @TableField(fill = INSERT_UPDATE) private Date lastUpdateTime; } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/mapper/RoleMapper.java ================================================ package com.xkcoding.orm.mybatis.plus.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xkcoding.orm.mybatis.plus.entity.Role; /** *

    * RoleMapper *

    * * @author yangkai.shen * @date Created in 2019-09-14 14:06 */ public interface RoleMapper extends BaseMapper { } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/mapper/UserMapper.java ================================================ package com.xkcoding.orm.mybatis.plus.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xkcoding.orm.mybatis.plus.entity.User; import org.springframework.stereotype.Component; /** *

    * UserMapper *

    * * @author yangkai.shen * @date Created in 2018-11-08 16:57 */ @Component public interface UserMapper extends BaseMapper { } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/service/UserService.java ================================================ package com.xkcoding.orm.mybatis.plus.service; import com.baomidou.mybatisplus.extension.service.IService; import com.xkcoding.orm.mybatis.plus.entity.User; /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-08 18:10 */ public interface UserService extends IService { } ================================================ FILE: demo-orm-mybatis-plus/src/main/java/com/xkcoding/orm/mybatis/plus/service/impl/UserServiceImpl.java ================================================ package com.xkcoding.orm.mybatis.plus.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xkcoding.orm.mybatis.plus.entity.User; import com.xkcoding.orm.mybatis.plus.mapper.UserMapper; import com.xkcoding.orm.mybatis.plus.service.UserService; import org.springframework.stereotype.Service; /** *

    * User Service *

    * * @author yangkai.shen * @date Created in 2018-11-08 18:10 */ @Service public class UserServiceImpl extends ServiceImpl implements UserService { } ================================================ FILE: demo-orm-mybatis-plus/src/main/resources/application.yml ================================================ spring: datasource: 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 initialization-mode: always continue-on-error: true schema: - "classpath:db/schema.sql" data: - "classpath:db/data.sql" hikari: minimum-idle: 5 connection-test-query: SELECT 1 FROM DUAL maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 logging: level: com.xkcoding: debug com.xkcoding.orm.mybatis.plus.mapper: trace mybatis-plus: mapper-locations: classpath:mappers/*.xml #实体扫描,多个package用逗号或者分号分隔 typeAliasesPackage: com.xkcoding.orm.mybatis.plus.entity global-config: # 数据库相关配置 db-config: #主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID"; id-type: auto #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断" field-strategy: not_empty #驼峰下划线转换 table-underline: true #是否开启大写命名,默认不开启 #capital-mode: true #逻辑删除配置 #logic-delete-value: 1 #logic-not-delete-value: 0 db-type: mysql #刷新mapper 调试神器 refresh: true # 原生配置 configuration: map-underscore-to-camel-case: true cache-enabled: true ================================================ FILE: demo-orm-mybatis-plus/src/main/resources/db/data.sql ================================================ INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (1, 'user_1', 'ff342e862e7c3285cdc07e56d6b8973b', '412365a109674b2dbb1981ed561a4c70', 'user1@xkcoding.com', '17300000001'); INSERT INTO `orm_user`(`id`,`name`,`password`,`salt`,`email`,`phone_number`) VALUES (2, 'user_2', '6c6bf02c8d5d3d128f34b1700cb1e32c', 'fcbdd0e8a9404a5585ea4e01d0e4d7a0', 'user2@xkcoding.com', '17300000002'); INSERT INTO `orm_role`(`id`,`name`) VALUES (1,'管理员'),(2,'普通用户'); ================================================ FILE: demo-orm-mybatis-plus/src/main/resources/db/schema.sql ================================================ DROP TABLE IF EXISTS `orm_user`; CREATE TABLE `orm_user` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(32) NOT NULL COMMENT '加密后的密码', `salt` VARCHAR(32) NOT NULL COMMENT '加密使用的盐', `email` VARCHAR(32) NOT NULL UNIQUE COMMENT '邮箱', `phone_number` VARCHAR(15) NOT NULL UNIQUE COMMENT '手机号码', `status` INT(2) NOT NULL DEFAULT 1 COMMENT '状态,-1:逻辑删除,0:禁用,1:启用', `create_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '创建时间', `last_login_time` DATETIME DEFAULT NULL COMMENT '上次登录时间', `last_update_time` DATETIME NOT NULL DEFAULT NOW() COMMENT '上次更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; DROP TABLE IF EXISTS `orm_role`; CREATE TABLE `orm_role` ( `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', `name` VARCHAR(32) NOT NULL UNIQUE COMMENT '角色名' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Spring Boot Demo Orm 系列示例表'; ================================================ FILE: demo-orm-mybatis-plus/src/test/java/com/xkcoding/orm/mybatis/plus/SpringBootDemoOrmMybatisPlusApplicationTests.java ================================================ package com.xkcoding.orm.mybatis.plus; 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 SpringBootDemoOrmMybatisPlusApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-orm-mybatis-plus/src/test/java/com/xkcoding/orm/mybatis/plus/activerecord/ActiveRecordTest.java ================================================ package com.xkcoding.orm.mybatis.plus.activerecord; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.xkcoding.orm.mybatis.plus.SpringBootDemoOrmMybatisPlusApplicationTests; import com.xkcoding.orm.mybatis.plus.entity.Role; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import java.util.List; /** *

    * Role *

    * * @author yangkai.shen * @date Created in 2019-09-14 14:19 */ @Slf4j public class ActiveRecordTest extends SpringBootDemoOrmMybatisPlusApplicationTests { /** * 测试 ActiveRecord 插入数据 */ @Test public void testActiveRecordInsert() { Role role = new Role(); role.setName("VIP"); Assert.assertTrue(role.insert()); // 成功直接拿会写的 ID log.debug("【role】= {}", role); } /** * 测试 ActiveRecord 更新数据 */ @Test public void testActiveRecordUpdate() { Assert.assertTrue(new Role().setId(1L).setName("管理员-1").updateById()); Assert.assertTrue(new Role().update(new UpdateWrapper().lambda().set(Role::getName, "普通用户-1").eq(Role::getId, 2))); } /** * 测试 ActiveRecord 查询数据 */ @Test public void testActiveRecordSelect() { Assert.assertEquals("管理员", new Role().setId(1L).selectById().getName()); Role role = new Role().selectOne(new QueryWrapper().lambda().eq(Role::getId, 2)); Assert.assertEquals("普通用户", role.getName()); List roles = new Role().selectAll(); Assert.assertTrue(roles.size() > 0); log.debug("【roles】= {}", roles); } /** * 测试 ActiveRecord 删除数据 */ @Test public void testActiveRecordDelete() { Assert.assertTrue(new Role().setId(1L).deleteById()); Assert.assertTrue(new Role().delete(new QueryWrapper().lambda().eq(Role::getName, "普通用户"))); } } ================================================ FILE: demo-orm-mybatis-plus/src/test/java/com/xkcoding/orm/mybatis/plus/service/UserServiceTest.java ================================================ package com.xkcoding.orm.mybatis.plus.service; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateTime; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.SecureUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.xkcoding.orm.mybatis.plus.SpringBootDemoOrmMybatisPlusApplicationTests; import com.xkcoding.orm.mybatis.plus.entity.User; import lombok.extern.slf4j.Slf4j; import org.assertj.core.util.Lists; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; import java.util.stream.Collectors; /** *

    * User Service 测试 *

    * * @author yangkai.shen * @date Created in 2018-11-08 18:13 */ @Slf4j public class UserServiceTest extends SpringBootDemoOrmMybatisPlusApplicationTests { @Autowired private UserService userService; /** * 测试Mybatis-Plus 新增 */ @Test public void testSave() { String salt = IdUtil.fastSimpleUUID(); User testSave3 = User.builder().name("testSave3").password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave3@xkcoding.com").phoneNumber("17300000003").status(1).lastLoginTime(new DateTime()).build(); boolean save = userService.save(testSave3); Assert.assertTrue(save); log.debug("【测试id回显#testSave3.getId()】= {}", testSave3.getId()); } /** * 测试Mybatis-Plus 批量新增 */ @Test public void testSaveList() { List userList = Lists.newArrayList(); for (int i = 4; i < 14; i++) { String salt = IdUtil.fastSimpleUUID(); User user = User.builder().name("testSave" + i).password(SecureUtil.md5("123456" + salt)).salt(salt).email("testSave" + i + "@xkcoding.com").phoneNumber("1730000000" + i).status(1).lastLoginTime(new DateTime()).build(); userList.add(user); } boolean batch = userService.saveBatch(userList); Assert.assertTrue(batch); List ids = userList.stream().map(User::getId).collect(Collectors.toList()); log.debug("【userList#ids】= {}", ids); } /** * 测试Mybatis-Plus 删除 */ @Test public void testDelete() { boolean remove = userService.removeById(1L); Assert.assertTrue(remove); User byId = userService.getById(1L); Assert.assertNull(byId); } /** * 测试Mybatis-Plus 修改 */ @Test public void testUpdate() { User user = userService.getById(1L); Assert.assertNotNull(user); user.setName("MybatisPlus修改名字"); boolean b = userService.updateById(user); Assert.assertTrue(b); User update = userService.getById(1L); Assert.assertEquals("MybatisPlus修改名字", update.getName()); log.debug("【update】= {}", update); } /** * 测试Mybatis-Plus 查询单个 */ @Test public void testQueryOne() { User user = userService.getById(1L); Assert.assertNotNull(user); log.debug("【user】= {}", user); } /** * 测试Mybatis-Plus 查询全部 */ @Test public void testQueryAll() { List list = userService.list(new QueryWrapper<>()); Assert.assertTrue(CollUtil.isNotEmpty(list)); log.debug("【list】= {}", list); } /** * 测试Mybatis-Plus 分页排序查询 */ @Test public void testQueryByPageAndSort() { initData(); int count = userService.count(new QueryWrapper<>()); Page userPage = new Page<>(1, 5); userPage.setDesc("id"); IPage page = userService.page(userPage, new QueryWrapper<>()); Assert.assertEquals(5, page.getSize()); Assert.assertEquals(count, page.getTotal()); log.debug("【page.getRecords()】= {}", page.getRecords()); } /** * 测试Mybatis-Plus 自定义查询 */ @Test public void testQueryByCondition() { initData(); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "Save1").or().eq("phone_number", "17300000001").orderByDesc("id"); int count = userService.count(wrapper); Page userPage = new Page<>(1, 3); IPage page = userService.page(userPage, wrapper); Assert.assertEquals(3, page.getSize()); Assert.assertEquals(count, page.getTotal()); log.debug("【page.getRecords()】= {}", page.getRecords()); } /** * 初始化数据 */ private void initData() { testSaveList(); } } ================================================ FILE: demo-pay/.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-pay/README.md ================================================ ================================================ FILE: demo-pay/pom.xml ================================================ 4.0.0 demo-pay 1.0.0-SNAPSHOT jar demo-pay Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.7.0 3.4.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.github.javen205 IJPay-All ${ijpay.version} com.google.zxing core ${zxing.version} com.google.zxing javase ${zxing.version} com.alipay.sdk alipay-sdk-java 4.10.159.ALL org.projectlombok lombok true demo-pay org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-pay/src/main/java/com/xkcoding/pay/SpringBootDemoPayApplication.java ================================================ package com.xkcoding.pay; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RestController; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2020-10-26 11:12 */ @SpringBootApplication @RestController public class SpringBootDemoPayApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoPayApplication.class, args); } } ================================================ FILE: demo-pay/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ================================================ FILE: demo-pay/src/test/java/com/xkcoding/pay/SpringBootDemoPayApplicationTests.java ================================================ package com.xkcoding.pay; 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 SpringBootDemoPayApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-properties/.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-properties/README.md ================================================ # spring-boot-demo-properties > 本 demo 演示如何获取配置文件的自定义配置,以及如何多环境下的配置文件信息的获取 ## pom.xml ```xml 4.0.0 spring-boot-demo-properties 1.0.0-SNAPSHOT jar spring-boot-demo-properties 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-configuration-processor true org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all spring-boot-demo-properties org.springframework.boot spring-boot-maven-plugin ``` ## ApplicationProperty.java ```java /** *

    * 项目配置 *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:50 */ @Data @Component public class ApplicationProperty { @Value("${application.name}") private String name; @Value("${application.version}") private String version; } ``` ## DeveloperProperty.java ```java /** *

    * 开发人员配置信息 *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:51 */ @Data @ConfigurationProperties(prefix = "developer") @Component public class DeveloperProperty { private String name; private String website; private String qq; private String phoneNumber; } ``` ## PropertyController.java ```java /** *

    * 测试Controller *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:49 */ @RestController public class PropertyController { private final ApplicationProperty applicationProperty; private final DeveloperProperty developerProperty; @Autowired public PropertyController(ApplicationProperty applicationProperty, DeveloperProperty developerProperty) { this.applicationProperty = applicationProperty; this.developerProperty = developerProperty; } @GetMapping("/property") public Dict index() { return Dict.create().set("applicationProperty", applicationProperty).set("developerProperty", developerProperty); } } ``` ## additional-spring-configuration-metadata.json > 位置: src/main/resources/META-INF/additional-spring-configuration-metadata.json ```json { "properties": [ { "name": "application.name", "description": "Default value is artifactId in pom.xml.", "type": "java.lang.String" }, { "name": "application.version", "description": "Default value is version in pom.xml.", "type": "java.lang.String" }, { "name": "developer.name", "description": "The Developer Name.", "type": "java.lang.String" }, { "name": "developer.website", "description": "The Developer Website.", "type": "java.lang.String" }, { "name": "developer.qq", "description": "The Developer QQ Number.", "type": "java.lang.String" }, { "name": "developer.phone-number", "description": "The Developer Phone Number.", "type": "java.lang.String" } ] } ``` ================================================ FILE: demo-properties/pom.xml ================================================ 4.0.0 demo-properties 1.0.0-SNAPSHOT jar demo-properties 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-configuration-processor true org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all demo-properties org.springframework.boot spring-boot-maven-plugin src/main/resources true ================================================ FILE: demo-properties/src/main/java/com/xkcoding/properties/SpringBootDemoPropertiesApplication.java ================================================ package com.xkcoding.properties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:48 */ @SpringBootApplication public class SpringBootDemoPropertiesApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoPropertiesApplication.class, args); } } ================================================ FILE: demo-properties/src/main/java/com/xkcoding/properties/controller/PropertyController.java ================================================ package com.xkcoding.properties.controller; import cn.hutool.core.lang.Dict; import com.xkcoding.properties.property.ApplicationProperty; import com.xkcoding.properties.property.DeveloperProperty; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** *

    * 测试Controller *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:49 */ @RestController public class PropertyController { private final ApplicationProperty applicationProperty; private final DeveloperProperty developerProperty; @Autowired public PropertyController(ApplicationProperty applicationProperty, DeveloperProperty developerProperty) { this.applicationProperty = applicationProperty; this.developerProperty = developerProperty; } @GetMapping("/property") public Dict index() { return Dict.create().set("applicationProperty", applicationProperty).set("developerProperty", developerProperty); } } ================================================ FILE: demo-properties/src/main/java/com/xkcoding/properties/property/ApplicationProperty.java ================================================ package com.xkcoding.properties.property; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** *

    * 项目配置 *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:50 */ @Data @Component public class ApplicationProperty { @Value("${application.name}") private String name; @Value("${application.version}") private String version; } ================================================ FILE: demo-properties/src/main/java/com/xkcoding/properties/property/DeveloperProperty.java ================================================ package com.xkcoding.properties.property; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** *

    * 开发人员配置信息 *

    * * @author yangkai.shen * @date Created in 2018-09-29 10:51 */ @Data @ConfigurationProperties(prefix = "developer") @Component public class DeveloperProperty { private String name; private String website; private String qq; private String phoneNumber; } ================================================ FILE: demo-properties/src/main/resources/META-INF/additional-spring-configuration-metadata.json ================================================ { "properties": [ { "name": "application.name", "description": "Default value is artifactId in pom.xml.", "type": "java.lang.String" }, { "name": "application.version", "description": "Default value is version in pom.xml.", "type": "java.lang.String" }, { "name": "developer.name", "description": "The Developer Name.", "type": "java.lang.String" }, { "name": "developer.website", "description": "The Developer Website.", "type": "java.lang.String" }, { "name": "developer.qq", "description": "The Developer QQ Number.", "type": "java.lang.String" }, { "name": "developer.phone-number", "description": "The Developer Phone Number.", "type": "java.lang.String" } ] } ================================================ FILE: demo-properties/src/main/resources/application-dev.yml ================================================ application: name: dev环境 @artifactId@ version: dev环境 @version@ developer: name: dev环境 xkcoding website: dev环境 http://xkcoding.com qq: dev环境 237497819 phone-number: dev环境 17326075631 ================================================ FILE: demo-properties/src/main/resources/application-prod.yml ================================================ application: name: prod环境 @artifactId@ version: prod环境 @version@ developer: name: prod环境 xkcoding website: prod环境 http://xkcoding.com qq: prod环境 237497819 phone-number: prod环境 17326075631 ================================================ FILE: demo-properties/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: profiles: active: prod ================================================ FILE: demo-properties/src/test/java/com/xkcoding/properties/SpringBootDemoPropertiesApplicationTests.java ================================================ package com.xkcoding.properties; 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 SpringBootDemoPropertiesApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-ratelimit-guava/.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-ratelimit-guava/README.md ================================================ # spring-boot-demo-ratelimit-guava > 此 demo 主要演示了 Spring Boot 项目如何通过 AOP 结合 Guava 的 RateLimiter 实现限流,旨在保护 API 被恶意频繁访问的问题。 ## 1. 主要代码 ### 1.1. pom.xml ```xml 4.0.0 spring-boot-demo-ratelimit-guava 1.0.0-SNAPSHOT jar spring-boot-demo-ratelimit-guava 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 cn.hutool hutool-all com.google.guava guava org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true spring-boot-demo-ratelimit-guava org.springframework.boot spring-boot-maven-plugin ``` ### 1.2. 定义一个限流注解 `RateLimiter.java` > 注意代码里使用了 `AliasFor` 设置一组属性的别名,所以获取注解的时候,需要通过 `Spring` 提供的注解工具类 `AnnotationUtils` 获取,不可以通过 `AOP` 参数注入的方式获取,否则有些属性的值将会设置不进去。 ```java /** *

    * 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效 * * @author yangkai.shen * @date Created in 2019-09-12 14:14 * @see AnnotationUtils *

    */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { int NOT_LIMITED = 0; /** * qps */ @AliasFor("qps") double value() default NOT_LIMITED; /** * qps */ @AliasFor("value") double qps() default NOT_LIMITED; /** * 超时时长 */ int timeout() default 0; /** * 超时时间单位 */ TimeUnit timeUnit() default TimeUnit.MILLISECONDS; } ``` ### 1.3. 定义一个切面 `RateLimiterAspect.java` ```java /** *

    * 限流切面 *

    * * @author yangkai.shen * @date Created in 2019-09-12 14:27 */ @Slf4j @Aspect @Component public class RateLimiterAspect { private static final ConcurrentMap RATE_LIMITER_CACHE = new ConcurrentHashMap<>(); @Pointcut("@annotation(com.xkcoding.ratelimit.guava.annotation.RateLimiter)") public void rateLimit() { } @Around("rateLimit()") public Object pointcut(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解 RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class); if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) { double qps = rateLimiter.qps(); if (RATE_LIMITER_CACHE.get(method.getName()) == null) { // 初始化 QPS RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps)); } log.debug("【{}】的QPS设置为: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate()); // 尝试获取令牌 if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) { throw new RuntimeException("手速太快了,慢点儿吧~"); } } return point.proceed(); } } ``` ### 1.4. 定义两个API接口用于测试限流 ```java /** *

    * 测试 *

    * * @author yangkai.shen * @date Created in 2019-09-12 14:22 */ @Slf4j @RestController public class TestController { @RateLimiter(value = 1.0, timeout = 300) @GetMapping("/test1") public Dict test1() { log.info("【test1】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } @GetMapping("/test2") public Dict test2() { log.info("【test2】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃"); } } ``` ## 2. 测试 - test1 接口未被限流的时候 image-20190912155209716 - test1 接口频繁刷新,触发限流的时候 image-20190912155229745 - test2 接口不做限流,可以一直刷新 image-20190912155146012 ## 3. 参考 - [限流原理解读之guava中的RateLimiter](https://juejin.im/post/5bb48d7b5188255c865e31bc) - [使用Guava的RateLimiter做限流](https://my.oschina.net/hanchao/blog/1833612) ================================================ FILE: demo-ratelimit-guava/pom.xml ================================================ 4.0.0 demo-ratelimit-guava 1.0.0-SNAPSHOT jar demo-ratelimit-guava 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 cn.hutool hutool-all com.google.guava guava org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true demo-ratelimit-guava org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-ratelimit-guava/src/main/java/com/xkcoding/ratelimit/guava/SpringBootDemoRatelimitGuavaApplication.java ================================================ package com.xkcoding.ratelimit.guava; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-09-12 14:06 */ @SpringBootApplication public class SpringBootDemoRatelimitGuavaApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoRatelimitGuavaApplication.class, args); } } ================================================ FILE: demo-ratelimit-guava/src/main/java/com/xkcoding/ratelimit/guava/annotation/RateLimiter.java ================================================ package com.xkcoding.ratelimit.guava.annotation; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationUtils; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** *

    * 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效 * * @author yangkai.shen * @date Created in 2019-09-12 14:14 * @see AnnotationUtils *

    */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { int NOT_LIMITED = 0; /** * qps */ @AliasFor("qps") double value() default NOT_LIMITED; /** * qps */ @AliasFor("value") double qps() default NOT_LIMITED; /** * 超时时长 */ int timeout() default 0; /** * 超时时间单位 */ TimeUnit timeUnit() default TimeUnit.MILLISECONDS; } ================================================ FILE: demo-ratelimit-guava/src/main/java/com/xkcoding/ratelimit/guava/aspect/RateLimiterAspect.java ================================================ package com.xkcoding.ratelimit.guava.aspect; import com.xkcoding.ratelimit.guava.annotation.RateLimiter; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; 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.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** *

    * 限流切面 *

    * * @author yangkai.shen * @date Created in 2019-09-12 14:27 */ @Slf4j @Aspect @Component public class RateLimiterAspect { private static final ConcurrentMap RATE_LIMITER_CACHE = new ConcurrentHashMap<>(); @Pointcut("@annotation(com.xkcoding.ratelimit.guava.annotation.RateLimiter)") public void rateLimit() { } @Around("rateLimit()") public Object pointcut(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解 RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class); if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) { double qps = rateLimiter.qps(); if (RATE_LIMITER_CACHE.get(method.getName()) == null) { // 初始化 QPS RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps)); } log.debug("【{}】的QPS设置为: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate()); // 尝试获取令牌 if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) { throw new RuntimeException("手速太快了,慢点儿吧~"); } } return point.proceed(); } } ================================================ FILE: demo-ratelimit-guava/src/main/java/com/xkcoding/ratelimit/guava/controller/TestController.java ================================================ package com.xkcoding.ratelimit.guava.controller; import cn.hutool.core.lang.Dict; import com.xkcoding.ratelimit.guava.annotation.RateLimiter; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** *

    * 测试 *

    * * @author yangkai.shen * @date Created in 2019-09-12 14:22 */ @Slf4j @RestController public class TestController { @RateLimiter(value = 1.0, timeout = 300) @GetMapping("/test1") public Dict test1() { log.info("【test1】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } @GetMapping("/test2") public Dict test2() { log.info("【test2】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃"); } @RateLimiter(value = 2.0, timeout = 300) @GetMapping("/test3") public Dict test3() { log.info("【test3】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } } ================================================ FILE: demo-ratelimit-guava/src/main/java/com/xkcoding/ratelimit/guava/handler/GlobalExceptionHandler.java ================================================ package com.xkcoding.ratelimit.guava.handler; import cn.hutool.core.lang.Dict; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** *

    * 全局异常拦截 *

    * * @author yangkai.shen * @date Created in 2019-09-12 15:00 */ @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public Dict handler(RuntimeException ex) { return Dict.create().set("msg", ex.getMessage()); } } ================================================ FILE: demo-ratelimit-guava/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo logging: level: com.xkcoding: debug ================================================ FILE: demo-ratelimit-guava/src/test/java/com/xkcoding/ratelimit/guava/SpringBootDemoRatelimitGuavaApplicationTests.java ================================================ package com.xkcoding.ratelimit.guava; 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 SpringBootDemoRatelimitGuavaApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-ratelimit-redis/.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-ratelimit-redis/README.md ================================================ # spring-boot-demo-ratelimit-redis > 此 demo 主要演示了 Spring Boot 项目如何通过 AOP 结合 Redis + Lua 脚本实现分布式限流,旨在保护 API 被恶意频繁访问的问题,是 `spring-boot-demo-ratelimit-guava` 的升级版。 ## 1. 主要代码 ### 1.1. pom.xml ```xml 4.0.0 spring-boot-demo-ratelimit-redis 1.0.0-SNAPSHOT jar spring-boot-demo-ratelimit-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-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 cn.hutool hutool-all org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true spring-boot-demo-ratelimit-redis org.springframework.boot spring-boot-maven-plugin ``` ### 1.2. 限流注解 ```java /** *

    * 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:31 * @see AnnotationUtils */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { long DEFAULT_REQUEST = 10; /** * max 最大请求数 */ @AliasFor("max") long value() default DEFAULT_REQUEST; /** * max 最大请求数 */ @AliasFor("value") long max() default DEFAULT_REQUEST; /** * 限流key */ String key() default ""; /** * 超时时长,默认1分钟 */ long timeout() default 1; /** * 超时时间单位,默认 分钟 */ TimeUnit timeUnit() default TimeUnit.MINUTES; } ``` ### 1.3. AOP处理限流 ```java /** *

    * 限流切面 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:30 */ @Slf4j @Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class RateLimiterAspect { private final static String SEPARATOR = ":"; private final static String REDIS_LIMIT_KEY_PREFIX = "limit:"; private final StringRedisTemplate stringRedisTemplate; private final RedisScript limitRedisScript; @Pointcut("@annotation(com.xkcoding.ratelimit.redis.annotation.RateLimiter)") public void rateLimit() { } @Around("rateLimit()") public Object pointcut(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解 RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class); if (rateLimiter != null) { String key = rateLimiter.key(); // 默认用类名+方法名做限流的 key 前缀 if (StrUtil.isBlank(key)) { key = method.getDeclaringClass().getName()+StrUtil.DOT+method.getName(); } // 最终限流的 key 为 前缀 + IP地址 // TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理 key = key + SEPARATOR + IpUtil.getIpAddr(); long max = rateLimiter.max(); long timeout = rateLimiter.timeout(); TimeUnit timeUnit = rateLimiter.timeUnit(); boolean limited = shouldLimited(key, max, timeout, timeUnit); if (limited) { throw new RuntimeException("手速太快了,慢点儿吧~"); } } return point.proceed(); } private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) { // 最终的 key 格式为: // limit:自定义key:IP // limit:类名.方法名:IP key = REDIS_LIMIT_KEY_PREFIX + key; // 统一使用单位毫秒 long ttl = timeUnit.toMillis(timeout); // 当前时间毫秒数 long now = Instant.now().toEpochMilli(); long expired = now - ttl; // 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + ""); if (executeTimes != null) { if (executeTimes == 0) { log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max); return true; } else { log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes); return false; } } return false; } } ``` ### 1.4. lua 脚本 ```lua -- 下标从 1 开始 local key = KEYS[1] local now = tonumber(ARGV[1]) local ttl = tonumber(ARGV[2]) local expired = tonumber(ARGV[3]) -- 最大访问量 local max = tonumber(ARGV[4]) -- 清除过期的数据 -- 移除指定分数区间内的所有元素,expired 即已经过期的 score -- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired redis.call('zremrangebyscore', key, 0, expired) -- 获取 zset 中的当前元素个数 local current = tonumber(redis.call('zcard', key)) local next = current + 1 if next > max then -- 达到限流大小 返回 0 return 0; else -- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score] redis.call("zadd", key, now, now) -- 每次访问均重新设置 zset 的过期时间,单位毫秒 redis.call("pexpire", key, ttl) return next end ``` ### 1.5. 接口测试 ```java /** *

    * 测试 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:30 */ @Slf4j @RestController public class TestController { @RateLimiter(value = 5) @GetMapping("/test1") public Dict test1() { log.info("【test1】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } @GetMapping("/test2") public Dict test2() { log.info("【test2】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃"); } @RateLimiter(value = 2, key = "测试自定义key") @GetMapping("/test3") public Dict test3() { log.info("【test3】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } } ``` ### 1.6. 其余代码参见 demo ## 2. 测试 - 触发限流时控制台打印 ![image-20190930155856711](http://static.xkcoding.com/spring-boot-demo/ratelimit/redis/063812.jpg) - 触发限流的时候 Redis 的数据 ![image-20190930155735300](http://static.xkcoding.com/spring-boot-demo/ratelimit/redis/063813.jpg) ## 3. 参考 - [mica-plus-redis 的分布式限流实现](https://github.com/lets-mica/mica/tree/master/mica-plus-redis) - [Java并发:分布式应用限流 Redis + Lua 实践](https://segmentfault.com/a/1190000016042927) ================================================ FILE: demo-ratelimit-redis/pom.xml ================================================ 4.0.0 demo-ratelimit-redis 1.0.0-SNAPSHOT jar demo-ratelimit-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-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 cn.hutool hutool-all org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true demo-ratelimit-redis org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/SpringBootDemoRatelimitRedisApplication.java ================================================ package com.xkcoding.ratelimit.redis; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-09-30 09:32 */ @SpringBootApplication public class SpringBootDemoRatelimitRedisApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoRatelimitRedisApplication.class, args); } } ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/annotation/RateLimiter.java ================================================ package com.xkcoding.ratelimit.redis.annotation; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationUtils; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** *

    * 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:31 * @see AnnotationUtils */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { long DEFAULT_REQUEST = 10; /** * max 最大请求数 */ @AliasFor("max") long value() default DEFAULT_REQUEST; /** * max 最大请求数 */ @AliasFor("value") long max() default DEFAULT_REQUEST; /** * 限流key */ String key() default ""; /** * 超时时长,默认1分钟 */ long timeout() default 1; /** * 超时时间单位,默认 分钟 */ TimeUnit timeUnit() default TimeUnit.MINUTES; } ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java ================================================ package com.xkcoding.ratelimit.redis.aspect; import cn.hutool.core.util.StrUtil; import com.xkcoding.ratelimit.redis.annotation.RateLimiter; import com.xkcoding.ratelimit.redis.util.IpUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; 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.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.Instant; import java.util.Collections; import java.util.concurrent.TimeUnit; /** *

    * 限流切面 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:30 */ @Slf4j @Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class RateLimiterAspect { private final static String SEPARATOR = ":"; private final static String REDIS_LIMIT_KEY_PREFIX = "limit:"; private final StringRedisTemplate stringRedisTemplate; private final RedisScript limitRedisScript; @Pointcut("@annotation(com.xkcoding.ratelimit.redis.annotation.RateLimiter)") public void rateLimit() { } @Around("rateLimit()") public Object pointcut(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解 RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class); if (rateLimiter != null) { String key = rateLimiter.key(); // 默认用类名+方法名做限流的 key 前缀 if (StrUtil.isBlank(key)) { key = method.getDeclaringClass().getName() + StrUtil.DOT + method.getName(); } // 最终限流的 key 为 前缀 + IP地址 // TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理 key = key + SEPARATOR + IpUtil.getIpAddr(); long max = rateLimiter.max(); long timeout = rateLimiter.timeout(); TimeUnit timeUnit = rateLimiter.timeUnit(); boolean limited = shouldLimited(key, max, timeout, timeUnit); if (limited) { throw new RuntimeException("手速太快了,慢点儿吧~"); } } return point.proceed(); } private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) { // 最终的 key 格式为: // limit:自定义key:IP // limit:类名.方法名:IP key = REDIS_LIMIT_KEY_PREFIX + key; // 统一使用单位毫秒 long ttl = timeUnit.toMillis(timeout); // 当前时间毫秒数 long now = Instant.now().toEpochMilli(); long expired = now - ttl; // 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + ""); if (executeTimes != null) { if (executeTimes == 0) { log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max); return true; } else { log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes); return false; } } return false; } } ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/config/RedisConfig.java ================================================ package com.xkcoding.ratelimit.redis.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.scripting.support.ResourceScriptSource; /** *

    * Redis 配置 *

    * * @author yangkai.shen * @date Created in 2019-09-30 11:37 */ @Configuration public class RedisConfig { @Bean @SuppressWarnings("unchecked") public RedisScript limitRedisScript() { DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/redis/limit.lua"))); redisScript.setResultType(Long.class); return redisScript; } } ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/controller/TestController.java ================================================ package com.xkcoding.ratelimit.redis.controller; import cn.hutool.core.lang.Dict; import com.xkcoding.ratelimit.redis.annotation.RateLimiter; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** *

    * 测试 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:30 */ @Slf4j @RestController public class TestController { @RateLimiter(value = 5) @GetMapping("/test1") public Dict test1() { log.info("【test1】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } @GetMapping("/test2") public Dict test2() { log.info("【test2】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃"); } @RateLimiter(value = 2, key = "测试自定义key") @GetMapping("/test3") public Dict test3() { log.info("【test3】被执行了。。。。。"); return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~"); } } ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/handler/GlobalExceptionHandler.java ================================================ package com.xkcoding.ratelimit.redis.handler; import cn.hutool.core.lang.Dict; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** *

    * 全局异常拦截 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:30 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public Dict handler(RuntimeException ex) { return Dict.create().set("msg", ex.getMessage()); } } ================================================ FILE: demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/util/IpUtil.java ================================================ package com.xkcoding.ratelimit.redis.util; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** *

    * IP 工具类 *

    * * @author yangkai.shen * @date Created in 2019-09-30 10:38 */ @Slf4j public class IpUtil { private final static String UNKNOWN = "unknown"; private final static int MAX_LENGTH = 15; /** * 获取IP地址 * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 */ public static String getIpAddr() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = null; try { ip = request.getHeader("x-forwarded-for"); if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StrUtil.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { log.error("IPUtils ERROR ", e); } // 使用代理,则获取第一个IP地址 if (!StrUtil.isEmpty(ip) && ip.length() > MAX_LENGTH) { if (ip.indexOf(StrUtil.COMMA) > 0) { ip = ip.substring(0, ip.indexOf(StrUtil.COMMA)); } } return ip; } } ================================================ FILE: demo-ratelimit-redis/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo 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 ================================================ FILE: demo-ratelimit-redis/src/main/resources/scripts/redis/limit.lua ================================================ -- 下标从 1 开始 local key = KEYS[1] local now = tonumber(ARGV[1]) local ttl = tonumber(ARGV[2]) local expired = tonumber(ARGV[3]) -- 最大访问量 local max = tonumber(ARGV[4]) -- 清除过期的数据 -- 移除指定分数区间内的所有元素,expired 即已经过期的 score -- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired redis.call('zremrangebyscore', key, 0, expired) -- 获取 zset 中的当前元素个数 local current = tonumber(redis.call('zcard', key)) local next = current + 1 if next > max then -- 达到限流大小 返回 0 return 0; else -- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score] redis.call("zadd", key, now, now) -- 每次访问均重新设置 zset 的过期时间,单位毫秒 redis.call("pexpire", key, ttl) return next end ================================================ FILE: demo-ratelimit-redis/src/test/java/com/xkcoding/ratelimit/redis/SpringBootDemoRatelimiterRedisApplicationTests.java ================================================ package com.xkcoding.ratelimit.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 SpringBootDemoRatelimiterRedisApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-rbac-security/.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-rbac-security/README.md ================================================ # spring-boot-demo-rbac-security > 此 demo 主要演示了 Spring Boot 项目如何集成 Spring Security 完成权限拦截操作。本 demo 为基于**前后端分离**的后端权限管理部分,不同于其他博客里使用的模板技术,希望对大家有所帮助。 ## 1. 主要功能 - [x] 基于 `RBAC` 权限模型设计,详情参考数据库表结构设计 [`security.sql`](./sql/security.sql) - [x] 支持**动态权限管理**,详情参考 [`RbacAuthorityService.java`](./src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java) - [x] **登录 / 登出**部分均使用自定义 Controller 实现,未使用 `Spring Security` 内部默认的实现,适用于前后端分离项目,详情参考 [`SecurityConfig.java`](./src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java) 和 [`AuthController.java`](./src/main/java/com/xkcoding/rbac/security/controller/AuthController.java) - [x] 持久化技术使用 `spring-data-jpa` 完成 - [x] 使用 `JWT` 实现安全验证,同时引入 `Redis` 解决 `JWT` 无法手动设置过期的弊端,并且保证同一用户在同一时间仅支持同一设备登录,不同设备登录会将,详情参考 [`JwtUtil.java`](./src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java) - [x] 在线人数统计,详情参考 [`MonitorService.java`](./src/main/java/com/xkcoding/rbac/security/service/MonitorService.java) 和 [`RedisUtil.java`](./src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java) - [x] 手动踢出用户,详情参考 [`MonitorService.java`](./src/main/java/com/xkcoding/rbac/security/service/MonitorService.java) - [x] 自定义配置不需要进行拦截的请求,详情参考 [`CustomConfig.java`](./src/main/java/com/xkcoding/rbac/security/config/CustomConfig.java) 和 [`application.yml`](./src/main/resources/application.yml) ## 2. 运行 ### 2.1. 环境 1. JDK 1.8 以上 2. Maven 3.5 以上 3. Mysql 5.7 以上 4. Redis ### 2.2. 运行方式 1. 新建一个名为 `spring-boot-demo` 的数据库,字符集设置为 `utf-8`,如果数据库名不是 `spring-boot-demo` 需要在 `application.yml` 中修改 `spring.datasource.url` 2. 使用 [`security.sql`](./sql/security.sql) 这个 SQL 文件,创建数据库表和初始化RBAC数据 3. 运行 `SpringBootDemoRbacSecurityApplication` 4. 管理员账号:admin/123456 普通用户:user/123456 5. 使用 `POST` 请求访问 `/${contextPath}/api/auth/login` 端点,输入账号密码,登陆成功之后返回token,将获得的 token 放在具体请求的 Header 里,key 固定是 `Authorization` ,value 前缀为 `Bearer 后面加空格`再加token,并加上具体请求的参数,就可以了 6. enjoy ~​ :kissing_smiling_eyes: ## 3. 部分关键代码 ### 3.1. pom.xml ```xml 4.0.0 spring-boot-demo-rbac-security 1.0.0-SNAPSHOT jar spring-boot-demo-rbac-security Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 0.9.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true io.jsonwebtoken jjwt ${jjwt.veersion} mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all org.projectlombok lombok true spring-boot-demo-rbac-security org.springframework.boot spring-boot-maven-plugin ``` ### 3.2. JwtUtil.java > JWT 工具类,主要功能:生成JWT并存入Redis、解析JWT并校验其准确性、从Request的Header中获取JWT ```java /** *

    * JWT 工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-07 13:42 */ @EnableConfigurationProperties(JwtConfig.class) @Configuration @Slf4j public class JwtUtil { @Autowired private JwtConfig jwtConfig; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 创建JWT * * @param rememberMe 记住我 * @param id 用户id * @param subject 用户名 * @param roles 用户角色 * @param authorities 用户权限 * @return JWT */ public String createJWT(Boolean rememberMe, Long id, String subject, List roles, Collection authorities) { Date now = new Date(); JwtBuilder builder = Jwts.builder() .setId(id.toString()) .setSubject(subject) .setIssuedAt(now) .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()) .claim("roles", roles) .claim("authorities", authorities); // 设置过期时间 Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl(); if (ttl > 0) { builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue())); } String jwt = builder.compact(); // 将生成的JWT保存至Redis stringRedisTemplate.opsForValue() .set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS); return jwt; } /** * 创建JWT * * @param authentication 用户认证信息 * @param rememberMe 记住我 * @return JWT */ public String createJWT(Authentication authentication, Boolean rememberMe) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities()); } /** * 解析JWT * * @param jwt JWT * @return {@link Claims} */ public Claims parseJWT(String jwt) { try { Claims claims = Jwts.parser() .setSigningKey(jwtConfig.getKey()) .parseClaimsJws(jwt) .getBody(); String username = claims.getSubject(); String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username; // 校验redis中的JWT是否存在 Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); if (Objects.isNull(expire) || expire <= 0) { throw new SecurityException(Status.TOKEN_EXPIRED); } // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期 String redisToken = stringRedisTemplate.opsForValue() .get(redisKey); if (!StrUtil.equals(jwt, redisToken)) { throw new SecurityException(Status.TOKEN_OUT_OF_CTRL); } return claims; } catch (ExpiredJwtException e) { log.error("Token 已过期"); throw new SecurityException(Status.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { log.error("不支持的 Token"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } catch (MalformedJwtException e) { log.error("Token 无效"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } catch (SignatureException e) { log.error("无效的 Token 签名"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } catch (IllegalArgumentException e) { log.error("Token 参数不存在"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } } /** * 设置JWT过期 * * @param request 请求 */ public void invalidateJWT(HttpServletRequest request) { String jwt = getJwtFromRequest(request); String username = getUsernameFromJWT(jwt); // 从redis中清除JWT stringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username); } /** * 根据 jwt 获取用户名 * * @param jwt JWT * @return 用户名 */ public String getUsernameFromJWT(String jwt) { Claims claims = parseJWT(jwt); return claims.getSubject(); } /** * 从 request 的 header 中获取 JWT * * @param request 请求 * @return JWT */ public String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` ### 3.3. SecurityConfig.java > Spring Security 配置类,主要功能:配置哪些URL不需要认证,哪些需要认证 ```java /** *

    * Security 配置 *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:46 */ @Configuration @EnableWebSecurity @EnableConfigurationProperties(CustomConfig.class) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomConfig customConfig; @Autowired private AccessDeniedHandler accessDeniedHandler; @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserDetailsService) .passwordEncoder(encoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors() // 关闭 CSRF .and() .csrf() .disable() // 登录行为由自己实现,参考 AuthController#login .formLogin() .disable() .httpBasic() .disable() // 认证请求 .authorizeRequests() // 所有请求都需要登录访问 .anyRequest() .authenticated() // RBAC 动态 url 认证 .anyRequest() .access("@rbacAuthorityService.hasPermission(request,authentication)") // 登出行为由自己实现,参考 AuthController#logout .and() .logout() .disable() // Session 管理 .sessionManagement() // 因为使用了JWT,所以这里不管理Session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 异常处理 .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler); // 添加自定义 JWT 过滤器 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } /** * 放行所有不需要登录就可以访问的请求,参见 AuthController * 也可以在 {@link #configure(HttpSecurity)} 中配置 * {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()} */ @Override public void configure(WebSecurity web) { WebSecurity and = web.ignoring() .and(); // 忽略 GET customConfig.getIgnores() .getGet() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.GET, url)); // 忽略 POST customConfig.getIgnores() .getPost() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.POST, url)); // 忽略 DELETE customConfig.getIgnores() .getDelete() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.DELETE, url)); // 忽略 PUT customConfig.getIgnores() .getPut() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.PUT, url)); // 忽略 HEAD customConfig.getIgnores() .getHead() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.HEAD, url)); // 忽略 PATCH customConfig.getIgnores() .getPatch() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.PATCH, url)); // 忽略 OPTIONS customConfig.getIgnores() .getOptions() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.OPTIONS, url)); // 忽略 TRACE customConfig.getIgnores() .getTrace() .forEach(url -> and.ignoring() .antMatchers(HttpMethod.TRACE, url)); // 按照请求格式忽略 customConfig.getIgnores() .getPattern() .forEach(url -> and.ignoring() .antMatchers(url)); } } ``` ### 3.4. RbacAuthorityService.java > 路由动态鉴权类,主要功能: > > 1. 校验请求的合法性,排除404和405这两种异常请求 > 2. 根据当前请求路径与该用户可访问的资源做匹配,通过则可以访问,否则,不允许访问 ```java /** *

    * 动态路由认证 *

    * * @author yangkai.shen * @date Created in 2018-12-10 17:17 */ @Component public class RbacAuthorityService { @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Autowired private RequestMappingHandlerMapping mapping; public boolean hasPermission(HttpServletRequest request, Authentication authentication) { checkRequest(request); Object userInfo = authentication.getPrincipal(); boolean hasPermission = false; if (userInfo instanceof UserDetails) { UserPrincipal principal = (UserPrincipal) userInfo; Long userId = principal.getId(); List roles = roleDao.selectByUserId(userId); List roleIds = roles.stream() .map(Role::getId) .collect(Collectors.toList()); List permissions = permissionDao.selectByRoleIdList(roleIds); //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限 List btnPerms = permissions.stream() // 过滤页面权限 .filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON)) // 过滤 URL 为空 .filter(permission -> StrUtil.isNotBlank(permission.getUrl())) // 过滤 METHOD 为空 .filter(permission -> StrUtil.isNotBlank(permission.getMethod())) .collect(Collectors.toList()); for (Permission btnPerm : btnPerms) { AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod()); if (antPathMatcher.matches(request)) { hasPermission = true; break; } } return hasPermission; } else { return false; } } /** * 校验请求是否存在 * * @param request 请求 */ private void checkRequest(HttpServletRequest request) { // 获取当前 request 的方法 String currentMethod = request.getMethod(); Multimap urlMapping = allUrlMapping(); for (String uri : urlMapping.keySet()) { // 通过 AntPathRequestMatcher 匹配 url // 可以通过 2 种方式创建 AntPathRequestMatcher // 1:new AntPathRequestMatcher(uri,method) 这种方式可以直接判断方法是否匹配,因为这里我们把 方法不匹配 自定义抛出,所以,我们使用第2种方式创建 // 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径 AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri); if (antPathMatcher.matches(request)) { if (!urlMapping.get(uri) .contains(currentMethod)) { throw new SecurityException(Status.HTTP_BAD_METHOD); } else { return; } } } throw new SecurityException(Status.REQUEST_NOT_FOUND); } /** * 获取 所有URL Mapping,返回格式为{"/test":["GET","POST"],"/sys":["GET","DELETE"]} * * @return {@link ArrayListMultimap} 格式的 URL Mapping */ private Multimap allUrlMapping() { Multimap urlMapping = ArrayListMultimap.create(); // 获取url与类和方法的对应信息 Map handlerMethods = mapping.getHandlerMethods(); handlerMethods.forEach((k, v) -> { // 获取当前 key 下的获取所有URL Set url = k.getPatternsCondition() .getPatterns(); RequestMethodsRequestCondition method = k.getMethodsCondition(); // 为每个URL添加所有的请求方法 url.forEach(s -> urlMapping.putAll(s, method.getMethods() .stream() .map(Enum::toString) .collect(Collectors.toList()))); }); return urlMapping; } } ``` ### 3.5. JwtAuthenticationFilter.java > JWT 认证过滤器,主要功能: > > 1. 过滤不需要拦截的请求 > 2. 根据当前请求的JWT,认证用户身份信息 ```java /** *

    * Jwt 认证过滤器 *

    * * @author yangkai.shen * @date Created in 2018-12-10 15:15 */ @Component @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private JwtUtil jwtUtil; @Autowired private CustomConfig customConfig; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (checkIgnores(request)) { filterChain.doFilter(request, response); return; } String jwt = jwtUtil.getJwtFromRequest(request); if (StrUtil.isNotBlank(jwt)) { try { String username = jwtUtil.getUsernameFromJWT(jwt); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext() .setAuthentication(authentication); filterChain.doFilter(request, response); } catch (SecurityException e) { ResponseUtil.renderJson(response, e); } } else { ResponseUtil.renderJson(response, Status.UNAUTHORIZED, null); } } /** * 请求是否不需要进行权限拦截 * * @param request 当前请求 * @return true - 忽略,false - 不忽略 */ private boolean checkIgnores(HttpServletRequest request) { String method = request.getMethod(); HttpMethod httpMethod = HttpMethod.resolve(method); if (ObjectUtil.isNull(httpMethod)) { httpMethod = HttpMethod.GET; } Set ignores = Sets.newHashSet(); switch (httpMethod) { case GET: ignores.addAll(customConfig.getIgnores() .getGet()); break; case PUT: ignores.addAll(customConfig.getIgnores() .getPut()); break; case HEAD: ignores.addAll(customConfig.getIgnores() .getHead()); break; case POST: ignores.addAll(customConfig.getIgnores() .getPost()); break; case PATCH: ignores.addAll(customConfig.getIgnores() .getPatch()); break; case TRACE: ignores.addAll(customConfig.getIgnores() .getTrace()); break; case DELETE: ignores.addAll(customConfig.getIgnores() .getDelete()); break; case OPTIONS: ignores.addAll(customConfig.getIgnores() .getOptions()); break; default: break; } ignores.addAll(customConfig.getIgnores() .getPattern()); if (CollUtil.isNotEmpty(ignores)) { for (String ignore : ignores) { AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method); if (matcher.matches(request)) { return true; } } } return false; } } ``` ### 3.6. CustomUserDetailsService.java > 实现 `UserDetailsService` 接口,主要功能:根据用户名查询用户信息 ```java /** *

    * 自定义UserDetails查询 *

    * * @author yangkai.shen * @date Created in 2018-12-10 10:29 */ @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Override public UserDetails loadUserByUsername(String usernameOrEmailOrPhone) throws UsernameNotFoundException { User user = userDao.findByUsernameOrEmailOrPhone(usernameOrEmailOrPhone, usernameOrEmailOrPhone, usernameOrEmailOrPhone) .orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + usernameOrEmailOrPhone)); List roles = roleDao.selectByUserId(user.getId()); List roleIds = roles.stream() .map(Role::getId) .collect(Collectors.toList()); List permissions = permissionDao.selectByRoleIdList(roleIds); return UserPrincipal.create(user, roles, permissions); } } ``` ### 3.7. RedisUtil.java > 主要功能:根据key的格式分页获取Redis存在的key列表 ```java /** *

    * Redis工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-11 20:24 */ @Component @Slf4j public class RedisUtil { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率 * * @param patternKey key格式 * @param currentPage 当前页码 * @param pageSize 每页条数 * @return 分页获取指定格式key */ public PageResult findKeysForPage(String patternKey, int currentPage, int pageSize) { ScanOptions options = ScanOptions.scanOptions() .match(patternKey) .build(); RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory(); RedisConnection rc = factory.getConnection(); Cursor cursor = rc.scan(options); List result = Lists.newArrayList(); long tmpIndex = 0; int startIndex = (currentPage - 1) * pageSize; int end = currentPage * pageSize; while (cursor.hasNext()) { String key = new String(cursor.next()); if (tmpIndex >= startIndex && tmpIndex < end) { result.add(key); } tmpIndex++; } try { cursor.close(); RedisConnectionUtils.releaseConnection(rc, factory); } catch (Exception e) { log.warn("Redis连接关闭异常,", e); } return new PageResult<>(result, tmpIndex); } } ``` ### 3.8. MonitorService.java > 监控服务,主要功能:查询当前在线人数分页列表,手动踢出某个用户 ```java package com.xkcoding.rbac.security.service; import cn.hutool.core.util.StrUtil; import com.google.common.collect.Lists; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.PageResult; import com.xkcoding.rbac.security.model.User; import com.xkcoding.rbac.security.repository.UserDao; import com.xkcoding.rbac.security.util.RedisUtil; import com.xkcoding.rbac.security.vo.OnlineUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; /** *

    * 监控 Service *

    * * @author yangkai.shen * @date Created in 2018-12-12 00:55 */ @Service public class MonitorService { @Autowired private RedisUtil redisUtil; @Autowired private UserDao userDao; public PageResult onlineUser(Integer page, Integer size) { PageResult keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, page, size); List rows = keys.getRows(); Long total = keys.getTotal(); // 根据 redis 中键获取用户名列表 List usernameList = rows.stream() .map(s -> StrUtil.subAfter(s, Consts.REDIS_JWT_KEY_PREFIX, true)) .collect(Collectors.toList()); // 根据用户名查询用户信息 List userList = userDao.findByUsernameIn(usernameList); // 封装在线用户信息 List onlineUserList = Lists.newArrayList(); userList.forEach(user -> onlineUserList.add(OnlineUser.create(user))); return new PageResult<>(onlineUserList, total); } } ``` ### 3.9. 其余代码参见本 demo ## 4. 参考 1. Spring Security 官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/ 2. JWT 官网:https://jwt.io/ 3. JJWT开源工具参考:https://github.com/jwtk/jjwt#quickstart 4. 授权部分参考官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/#authorization 4. 动态授权部分,参考博客:https://blog.csdn.net/larger5/article/details/81063438 ================================================ FILE: demo-rbac-security/pom.xml ================================================ 4.0.0 demo-rbac-security 1.0.0-SNAPSHOT jar demo-rbac-security Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 0.9.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true io.jsonwebtoken jjwt ${jjwt.veersion} mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-rbac-security org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-rbac-security/sql/security.sql ================================================ /* Navicat Premium Data Transfer Source Server : 本机 Source Server Type : MySQL Source Server Version : 50718 Source Host : localhost:3306 Source Schema : spring-boot-demo Target Server Type : MySQL Target Server Version : 50718 File Encoding : 65001 Date: 12/12/2018 18:52:51 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sec_permission -- ---------------------------- DROP TABLE IF EXISTS `sec_permission`; CREATE TABLE `sec_permission` ( `id` bigint(64) NOT NULL COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '权限名', `url` varchar(1000) DEFAULT NULL COMMENT '类型为页面时,代表前端路由地址,类型为按钮时,代表后端接口地址', `type` int(2) NOT NULL COMMENT '权限类型,页面-1,按钮-2', `permission` varchar(50) DEFAULT NULL COMMENT '权限表达式', `method` varchar(50) DEFAULT NULL COMMENT '后端接口访问方式', `sort` int(11) NOT NULL COMMENT '排序', `parent_id` bigint(64) NOT NULL COMMENT '父级id', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='权限表'; -- ---------------------------- -- Records of sec_permission -- ---------------------------- BEGIN; INSERT INTO `sec_permission` VALUES (1072806379288399872, '测试页面', '/test', 1, 'page:test', NULL, 1, 0); INSERT INTO `sec_permission` VALUES (1072806379313565696, '测试页面-查询', '/**/test', 2, 'btn:test:query', 'GET', 1, 1072806379288399872); INSERT INTO `sec_permission` VALUES (1072806379330342912, '测试页面-添加', '/**/test', 2, 'btn:test:insert', 'POST', 2, 1072806379288399872); INSERT INTO `sec_permission` VALUES (1072806379342925824, '监控在线用户页面', '/monitor', 1, 'page:monitor:online', NULL, 2, 0); INSERT INTO `sec_permission` VALUES (1072806379363897344, '在线用户页面-查询', '/**/api/monitor/online/user', 2, 'btn:monitor:online:query', 'GET', 1, 1072806379342925824); INSERT INTO `sec_permission` VALUES (1072806379384868864, '在线用户页面-踢出', '/**/api/monitor/online/user/kickout', 2, 'btn:monitor:online:kickout', 'DELETE', 2, 1072806379342925824); COMMIT; -- ---------------------------- -- Table structure for sec_role -- ---------------------------- DROP TABLE IF EXISTS `sec_role`; CREATE TABLE `sec_role` ( `id` bigint(64) NOT NULL COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '角色名', `description` varchar(100) DEFAULT NULL COMMENT '描述', `create_time` bigint(13) NOT NULL COMMENT '创建时间', `update_time` bigint(13) NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='角色表'; -- ---------------------------- -- Records of sec_role -- ---------------------------- BEGIN; INSERT INTO `sec_role` VALUES (1072806379208708096, '管理员', '超级管理员', 1544611947239, 1544611947239); INSERT INTO `sec_role` VALUES (1072806379238068224, '普通用户', '普通用户', 1544611947246, 1544611947246); COMMIT; -- ---------------------------- -- Table structure for sec_role_permission -- ---------------------------- DROP TABLE IF EXISTS `sec_role_permission`; CREATE TABLE `sec_role_permission` ( `role_id` bigint(64) NOT NULL COMMENT '角色主键', `permission_id` bigint(64) NOT NULL COMMENT '权限主键', PRIMARY KEY (`role_id`, `permission_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='角色权限关系表'; -- ---------------------------- -- Records of sec_role_permission -- ---------------------------- BEGIN; INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379288399872); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379313565696); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379330342912); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379342925824); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379363897344); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379384868864); INSERT INTO `sec_role_permission` VALUES (1072806379238068224, 1072806379288399872); INSERT INTO `sec_role_permission` VALUES (1072806379238068224, 1072806379313565696); COMMIT; -- ---------------------------- -- Table structure for sec_user -- ---------------------------- DROP TABLE IF EXISTS `sec_user`; CREATE TABLE `sec_user` ( `id` bigint(64) NOT NULL COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(60) NOT NULL COMMENT '密码', `nickname` varchar(255) DEFAULT NULL COMMENT '昵称', `phone` varchar(11) DEFAULT NULL COMMENT '手机', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `birthday` bigint(13) DEFAULT NULL COMMENT '生日', `sex` int(2) DEFAULT NULL COMMENT '性别,男-1,女-2', `status` int(2) NOT NULL DEFAULT '1' COMMENT '状态,启用-1,禁用-0', `create_time` bigint(13) NOT NULL COMMENT '创建时间', `update_time` bigint(13) NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`), UNIQUE KEY `phone` (`phone`), UNIQUE KEY `email` (`email`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='用户表'; -- ---------------------------- -- Records of sec_user -- ---------------------------- BEGIN; INSERT INTO `sec_user` VALUES (1072806377661009920, 'admin', '$2a$10$64iuSLkKNhpTN19jGHs7xePvFsub7ZCcCmBqEYw8fbACGTE3XetYq', '管理员', '17300000000', 'admin@xkcoding.com', 785433600000, 1, 1, 1544611947032, 1544611947032); INSERT INTO `sec_user` VALUES (1072806378780889088, 'user', '$2a$10$OUDl4thpcHfs7WZ1kMUOb.ZO5eD4QANW5E.cexBLiKDIzDNt87QbO', '普通用户', '17300001111', 'user@xkcoding.com', 785433600000, 1, 1, 1544611947234, 1544611947234); COMMIT; -- ---------------------------- -- Table structure for sec_user_role -- ---------------------------- DROP TABLE IF EXISTS `sec_user_role`; CREATE TABLE `sec_user_role` ( `user_id` bigint(64) NOT NULL COMMENT '用户主键', `role_id` bigint(64) NOT NULL COMMENT '角色主键', PRIMARY KEY (`user_id`, `role_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='用户角色关系表'; -- ---------------------------- -- Records of sec_user_role -- ---------------------------- BEGIN; INSERT INTO `sec_user_role` VALUES (1072806377661009920, 1072806379208708096); INSERT INTO `sec_user_role` VALUES (1072806378780889088, 1072806379238068224); COMMIT; SET FOREIGN_KEY_CHECKS = 1; ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/SpringBootDemoRbacSecurityApplication.java ================================================ package com.xkcoding.rbac.security; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-12-10 11:28 */ @SpringBootApplication public class SpringBootDemoRbacSecurityApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoRbacSecurityApplication.class, args); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/ApiResponse.java ================================================ package com.xkcoding.rbac.security.common; import lombok.Data; import java.io.Serializable; /** *

    * 通用的 API 接口封装 *

    * * @author yangkai.shen * @date Created in 2018-12-07 14:55 */ @Data public class ApiResponse implements Serializable { private static final long serialVersionUID = 8993485788201922830L; /** * 状态码 */ 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返回 * * @return ApiResponse */ public static ApiResponse ofSuccess() { return ofSuccess(null); } /** * 构造一个成功且带数据的API返回 * * @param data 返回数据 * @return ApiResponse */ public static ApiResponse ofSuccess(Object data) { return ofStatus(Status.SUCCESS, data); } /** * 构造一个成功且自定义消息的API返回 * * @param message 返回内容 * @return ApiResponse */ public static ApiResponse ofMessage(String message) { return of(Status.SUCCESS.getCode(), message, null); } /** * 构造一个有状态的API返回 * * @param status 状态 {@link Status} * @return ApiResponse */ public static ApiResponse ofStatus(Status status) { return ofStatus(status, null); } /** * 构造一个有状态且带数据的API返回 * * @param status 状态 {@link IStatus} * @param data 返回数据 * @return ApiResponse */ public static ApiResponse ofStatus(IStatus status, Object data) { return of(status.getCode(), status.getMessage(), data); } /** * 构造一个异常的API返回 * * @param t 异常 * @param {@link BaseException} 的子类 * @return ApiResponse */ public static ApiResponse ofException(T t) { return of(t.getCode(), t.getMessage(), t.getData()); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java ================================================ package com.xkcoding.rbac.security.common; import lombok.Data; import lombok.EqualsAndHashCode; /** *

    * 异常基类 *

    * * @author yangkai.shen * @date Created in 2018-12-07 14:57 */ @EqualsAndHashCode(callSuper = true) @Data public class BaseException extends RuntimeException { private Integer code; private String message; private Object data; public BaseException(Status status) { super(status.getMessage()); this.code = status.getCode(); this.message = status.getMessage(); } public BaseException(Status status, Object data) { this(status); this.data = data; } public BaseException(Integer code, String message) { super(message); this.code = code; this.message = message; } public BaseException(Integer code, String message, Object data) { this(code, message); this.data = data; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java ================================================ package com.xkcoding.rbac.security.common; /** *

    * 常量池 *

    * * @author yangkai.shen * @date Created in 2018-12-10 15:03 */ public interface Consts { /** * 启用 */ Integer ENABLE = 1; /** * 禁用 */ Integer DISABLE = 0; /** * 页面 */ Integer PAGE = 1; /** * 按钮 */ Integer BUTTON = 2; /** * JWT 在 Redis 中保存的key前缀 */ String REDIS_JWT_KEY_PREFIX = "security:jwt:"; /** * 星号 */ String SYMBOL_STAR = "*"; /** * 邮箱符号 */ String SYMBOL_EMAIL = "@"; /** * 默认当前页码 */ Integer DEFAULT_CURRENT_PAGE = 1; /** * 默认每页条数 */ Integer DEFAULT_PAGE_SIZE = 10; /** * 匿名用户 用户名 */ String ANONYMOUS_NAME = "匿名用户"; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/IStatus.java ================================================ package com.xkcoding.rbac.security.common; /** *

    * REST API 错误码接口 *

    * * @author yangkai.shen * @date Created in 2018-12-07 14:35 */ public interface IStatus { /** * 状态码 * * @return 状态码 */ Integer getCode(); /** * 返回信息 * * @return 返回信息 */ String getMessage(); } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/PageResult.java ================================================ package com.xkcoding.rbac.security.common; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; /** *

    * 通用分页参数返回 *

    * * @author yangkai.shen * @date Created in 2018-12-11 20:26 */ @Data @NoArgsConstructor @AllArgsConstructor public class PageResult implements Serializable { private static final long serialVersionUID = 3420391142991247367L; /** * 当前页数据 */ private List rows; /** * 总条数 */ private Long total; public static PageResult of(List rows, Long total) { return new PageResult<>(rows, total); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java ================================================ package com.xkcoding.rbac.security.common; import lombok.Getter; /** *

    * 通用状态码 *

    * * @author yangkai.shen * @date Created in 2018-12-07 14:31 */ @Getter public enum Status implements IStatus { /** * 操作成功! */ SUCCESS(200, "操作成功!"), /** * 操作异常! */ ERROR(500, "操作异常!"), /** * 退出成功! */ LOGOUT(200, "退出成功!"), /** * 请先登录! */ UNAUTHORIZED(401, "请先登录!"), /** * 暂无权限访问! */ ACCESS_DENIED(403, "权限不足!"), /** * 请求不存在! */ REQUEST_NOT_FOUND(404, "请求不存在!"), /** * 请求方式不支持! */ HTTP_BAD_METHOD(405, "请求方式不支持!"), /** * 请求异常! */ BAD_REQUEST(400, "请求异常!"), /** * 参数不匹配! */ PARAM_NOT_MATCH(400, "参数不匹配!"), /** * 参数不能为空! */ PARAM_NOT_NULL(400, "参数不能为空!"), /** * 当前用户已被锁定,请联系管理员解锁! */ USER_DISABLED(403, "当前用户已被锁定,请联系管理员解锁!"), /** * 用户名或密码错误! */ USERNAME_PASSWORD_ERROR(5001, "用户名或密码错误!"), /** * token 已过期,请重新登录! */ TOKEN_EXPIRED(5002, "token 已过期,请重新登录!"), /** * token 解析失败,请尝试重新登录! */ TOKEN_PARSE_ERROR(5002, "token 解析失败,请尝试重新登录!"), /** * 当前用户已在别处登录,请尝试更改密码或重新登录! */ TOKEN_OUT_OF_CTRL(5003, "当前用户已在别处登录,请尝试更改密码或重新登录!"), /** * 无法手动踢出自己,请尝试退出登录操作! */ KICKOUT_SELF(5004, "无法手动踢出自己,请尝试退出登录操作!"); /** * 状态码 */ private Integer code; /** * 返回信息 */ private String message; Status(Integer code, String message) { this.code = code; this.message = message; } public static Status fromCode(Integer code) { Status[] statuses = Status.values(); for (Status status : statuses) { if (status.getCode().equals(code)) { return status; } } return SUCCESS; } @Override public String toString() { return String.format(" Status:{code=%s, message=%s} ", getCode(), getMessage()); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/CustomConfig.java ================================================ package com.xkcoding.rbac.security.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** *

    * 自定义配置 *

    * * @author yangkai.shen * @date Created in 2018-12-13 10:56 */ @ConfigurationProperties(prefix = "custom.config") @Data public class CustomConfig { /** * 不需要拦截的地址 */ private IgnoreConfig ignores; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/IdConfig.java ================================================ package com.xkcoding.rbac.security.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 2018-12-10 11:28 */ @Configuration public class IdConfig { /** * 雪花生成器 */ @Bean public Snowflake snowflake() { return IdUtil.createSnowflake(1, 1); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/IgnoreConfig.java ================================================ package com.xkcoding.rbac.security.config; import com.google.common.collect.Lists; import lombok.Data; import java.util.List; /** *

    * 忽略配置 *

    * * @author yangkai.shen * @date Created in 2018-12-17 17:37 */ @Data public class IgnoreConfig { /** * 需要忽略的 URL 格式,不考虑请求方法 */ private List pattern = Lists.newArrayList(); /** * 需要忽略的 GET 请求 */ private List get = Lists.newArrayList(); /** * 需要忽略的 POST 请求 */ private List post = Lists.newArrayList(); /** * 需要忽略的 DELETE 请求 */ private List delete = Lists.newArrayList(); /** * 需要忽略的 PUT 请求 */ private List put = Lists.newArrayList(); /** * 需要忽略的 HEAD 请求 */ private List head = Lists.newArrayList(); /** * 需要忽略的 PATCH 请求 */ private List patch = Lists.newArrayList(); /** * 需要忽略的 OPTIONS 请求 */ private List options = Lists.newArrayList(); /** * 需要忽略的 TRACE 请求 */ private List trace = Lists.newArrayList(); } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java ================================================ package com.xkcoding.rbac.security.config; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.google.common.collect.Sets; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.exception.SecurityException; import com.xkcoding.rbac.security.service.CustomUserDetailsService; import com.xkcoding.rbac.security.util.JwtUtil; import com.xkcoding.rbac.security.util.ResponseUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Set; /** *

    * Jwt 认证过滤器 *

    * * @author yangkai.shen * @date Created in 2018-12-10 15:15 */ @Component @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private JwtUtil jwtUtil; @Autowired private CustomConfig customConfig; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (checkIgnores(request)) { filterChain.doFilter(request, response); return; } String jwt = jwtUtil.getJwtFromRequest(request); if (StrUtil.isNotBlank(jwt)) { try { String username = jwtUtil.getUsernameFromJWT(jwt); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } catch (SecurityException e) { ResponseUtil.renderJson(response, e); } } else { ResponseUtil.renderJson(response, Status.UNAUTHORIZED, null); } } /** * 请求是否不需要进行权限拦截 * * @param request 当前请求 * @return true - 忽略,false - 不忽略 */ private boolean checkIgnores(HttpServletRequest request) { String method = request.getMethod(); HttpMethod httpMethod = HttpMethod.resolve(method); if (ObjectUtil.isNull(httpMethod)) { httpMethod = HttpMethod.GET; } Set ignores = Sets.newHashSet(); switch (httpMethod) { case GET: ignores.addAll(customConfig.getIgnores().getGet()); break; case PUT: ignores.addAll(customConfig.getIgnores().getPut()); break; case HEAD: ignores.addAll(customConfig.getIgnores().getHead()); break; case POST: ignores.addAll(customConfig.getIgnores().getPost()); break; case PATCH: ignores.addAll(customConfig.getIgnores().getPatch()); break; case TRACE: ignores.addAll(customConfig.getIgnores().getTrace()); break; case DELETE: ignores.addAll(customConfig.getIgnores().getDelete()); break; case OPTIONS: ignores.addAll(customConfig.getIgnores().getOptions()); break; default: break; } ignores.addAll(customConfig.getIgnores().getPattern()); if (CollUtil.isNotEmpty(ignores)) { for (String ignore : ignores) { AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method); if (matcher.matches(request)) { return true; } } } return false; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java ================================================ package com.xkcoding.rbac.security.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** *

    * JWT 配置 *

    * * @author yangkai.shen * @date Created in 2018-12-07 13:42 */ @ConfigurationProperties(prefix = "jwt.config") @Data public class JwtConfig { /** * jwt 加密 key,默认值:xkcoding. */ private String key = "xkcoding"; /** * jwt 过期时间,默认值:600000 {@code 10 分钟}. */ private Long ttl = 600000L; /** * 开启 记住我 之后 jwt 过期时间,默认值 604800000 {@code 7 天} */ private Long remember = 604800000L; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java ================================================ package com.xkcoding.rbac.security.config; import cn.hutool.core.util.StrUtil; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.exception.SecurityException; import com.xkcoding.rbac.security.model.Permission; import com.xkcoding.rbac.security.model.Role; import com.xkcoding.rbac.security.repository.PermissionDao; import com.xkcoding.rbac.security.repository.RoleDao; import com.xkcoding.rbac.security.vo.UserPrincipal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** *

    * 动态路由认证 *

    * * @author yangkai.shen * @date Created in 2018-12-10 17:17 */ @Component public class RbacAuthorityService { @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Autowired private RequestMappingHandlerMapping mapping; public boolean hasPermission(HttpServletRequest request, Authentication authentication) { checkRequest(request); Object userInfo = authentication.getPrincipal(); boolean hasPermission = false; if (userInfo instanceof UserDetails) { UserPrincipal principal = (UserPrincipal) userInfo; Long userId = principal.getId(); List roles = roleDao.selectByUserId(userId); List roleIds = roles.stream().map(Role::getId).collect(Collectors.toList()); List permissions = permissionDao.selectByRoleIdList(roleIds); //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限 List btnPerms = permissions.stream() // 过滤页面权限 .filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON)) // 过滤 URL 为空 .filter(permission -> StrUtil.isNotBlank(permission.getUrl())) // 过滤 METHOD 为空 .filter(permission -> StrUtil.isNotBlank(permission.getMethod())).collect(Collectors.toList()); for (Permission btnPerm : btnPerms) { AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod()); if (antPathMatcher.matches(request)) { hasPermission = true; break; } } return hasPermission; } else { return false; } } /** * 校验请求是否存在 * * @param request 请求 */ private void checkRequest(HttpServletRequest request) { // 获取当前 request 的方法 String currentMethod = request.getMethod(); Multimap urlMapping = allUrlMapping(); for (String uri : urlMapping.keySet()) { // 通过 AntPathRequestMatcher 匹配 url // 可以通过 2 种方式创建 AntPathRequestMatcher // 1:new AntPathRequestMatcher(uri,method) 这种方式可以直接判断方法是否匹配,因为这里我们把 方法不匹配 自定义抛出,所以,我们使用第2种方式创建 // 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径 AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri); if (antPathMatcher.matches(request)) { if (!urlMapping.get(uri).contains(currentMethod)) { throw new SecurityException(Status.HTTP_BAD_METHOD); } else { return; } } } throw new SecurityException(Status.REQUEST_NOT_FOUND); } /** * 获取 所有URL Mapping,返回格式为{"/test":["GET","POST"],"/sys":["GET","DELETE"]} * * @return {@link ArrayListMultimap} 格式的 URL Mapping */ private Multimap allUrlMapping() { Multimap urlMapping = ArrayListMultimap.create(); // 获取url与类和方法的对应信息 Map handlerMethods = mapping.getHandlerMethods(); handlerMethods.forEach((k, v) -> { // 获取当前 key 下的获取所有URL Set url = k.getPatternsCondition().getPatterns(); RequestMethodsRequestCondition method = k.getMethodsCondition(); // 为每个URL添加所有的请求方法 url.forEach(s -> urlMapping.putAll(s, method.getMethods().stream().map(Enum::toString).collect(Collectors.toList()))); }); return urlMapping; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java ================================================ package com.xkcoding.rbac.security.config; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.StringRedisSerializer; import java.io.Serializable; /** *

    * redis配置 *

    * * @author yangkai.shen * @date Created in 2018-12-11 15:16 */ @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; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java ================================================ package com.xkcoding.rbac.security.config; import com.xkcoding.rbac.security.service.CustomUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** *

    * Security 配置 *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:46 */ @Configuration @EnableWebSecurity @EnableConfigurationProperties(CustomConfig.class) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomConfig customConfig; @Autowired private AccessDeniedHandler accessDeniedHandler; @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public BCryptPasswordEncoder encoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserDetailsService).passwordEncoder(encoder()); } @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.cors() // 关闭 CSRF .and().csrf().disable() // 登录行为由自己实现,参考 AuthController#login .formLogin().disable() .httpBasic().disable() // 认证请求 .authorizeRequests() // 所有请求都需要登录访问 .anyRequest() .authenticated() // RBAC 动态 url 认证 .anyRequest() .access("@rbacAuthorityService.hasPermission(request,authentication)") // 登出行为由自己实现,参考 AuthController#logout .and().logout().disable() // Session 管理 .sessionManagement() // 因为使用了JWT,所以这里不管理Session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 异常处理 .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler); // @formatter:on // 添加自定义 JWT 过滤器 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } /** * 放行所有不需要登录就可以访问的请求,参见 AuthController * 也可以在 {@link #configure(HttpSecurity)} 中配置 * {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()} */ @Override public void configure(WebSecurity web) { WebSecurity and = web.ignoring().and(); // 忽略 GET customConfig.getIgnores().getGet().forEach(url -> and.ignoring().antMatchers(HttpMethod.GET, url)); // 忽略 POST customConfig.getIgnores().getPost().forEach(url -> and.ignoring().antMatchers(HttpMethod.POST, url)); // 忽略 DELETE customConfig.getIgnores().getDelete().forEach(url -> and.ignoring().antMatchers(HttpMethod.DELETE, url)); // 忽略 PUT customConfig.getIgnores().getPut().forEach(url -> and.ignoring().antMatchers(HttpMethod.PUT, url)); // 忽略 HEAD customConfig.getIgnores().getHead().forEach(url -> and.ignoring().antMatchers(HttpMethod.HEAD, url)); // 忽略 PATCH customConfig.getIgnores().getPatch().forEach(url -> and.ignoring().antMatchers(HttpMethod.PATCH, url)); // 忽略 OPTIONS customConfig.getIgnores().getOptions().forEach(url -> and.ignoring().antMatchers(HttpMethod.OPTIONS, url)); // 忽略 TRACE customConfig.getIgnores().getTrace().forEach(url -> and.ignoring().antMatchers(HttpMethod.TRACE, url)); // 按照请求格式忽略 customConfig.getIgnores().getPattern().forEach(url -> and.ignoring().antMatchers(url)); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java ================================================ package com.xkcoding.rbac.security.config; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.util.ResponseUtil; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.access.AccessDeniedHandler; /** *

    * Security 结果处理配置 *

    * * @author yangkai.shen * @date Created in 2018-12-07 17:31 */ @Configuration public class SecurityHandlerConfig { @Bean public AccessDeniedHandler accessDeniedHandler() { return (request, response, accessDeniedException) -> ResponseUtil.renderJson(response, Status.ACCESS_DENIED, null); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/WebMvcConfig.java ================================================ package com.xkcoding.rbac.security.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** *

    * MVC配置 *

    * * @author yangkai.shen * @date Created in 2018-12-10 16:09 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { private static final long MAX_AGE_SECS = 3600; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*").allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE").maxAge(MAX_AGE_SECS); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java ================================================ package com.xkcoding.rbac.security.controller; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.exception.SecurityException; import com.xkcoding.rbac.security.payload.LoginRequest; import com.xkcoding.rbac.security.util.JwtUtil; import com.xkcoding.rbac.security.vo.JwtResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; /** *

    * 认证 Controller,包括用户注册,用户登录请求 *

    * * @author yangkai.shen * @date Created in 2018-12-07 17:23 */ @Slf4j @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtil jwtUtil; /** * 登录 */ @PostMapping("/login") public ApiResponse login(@Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsernameOrEmailOrPhone(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtil.createJWT(authentication, loginRequest.getRememberMe()); return ApiResponse.ofSuccess(new JwtResponse(jwt)); } @PostMapping("/logout") public ApiResponse logout(HttpServletRequest request) { try { // 设置JWT过期 jwtUtil.invalidateJWT(request); } catch (SecurityException e) { throw new SecurityException(Status.UNAUTHORIZED); } return ApiResponse.ofStatus(Status.LOGOUT); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/MonitorController.java ================================================ package com.xkcoding.rbac.security.controller; import cn.hutool.core.collection.CollUtil; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.PageResult; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.exception.SecurityException; import com.xkcoding.rbac.security.payload.PageCondition; import com.xkcoding.rbac.security.service.MonitorService; import com.xkcoding.rbac.security.util.PageUtil; import com.xkcoding.rbac.security.util.SecurityUtil; import com.xkcoding.rbac.security.vo.OnlineUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** *

    * 监控 Controller,在线用户,手动踢出用户等功能 *

    * * @author yangkai.shen * @date Created in 2018-12-11 20:55 */ @Slf4j @RestController @RequestMapping("/api/monitor") public class MonitorController { @Autowired private MonitorService monitorService; /** * 在线用户列表 * * @param pageCondition 分页参数 */ @GetMapping("/online/user") public ApiResponse onlineUser(PageCondition pageCondition) { PageUtil.checkPageCondition(pageCondition, PageCondition.class); PageResult pageResult = monitorService.onlineUser(pageCondition); return ApiResponse.ofSuccess(pageResult); } /** * 批量踢出在线用户 * * @param names 用户名列表 */ @DeleteMapping("/online/user/kickout") public ApiResponse kickoutOnlineUser(@RequestBody List names) { if (CollUtil.isEmpty(names)) { throw new SecurityException(Status.PARAM_NOT_NULL); } if (names.contains(SecurityUtil.getCurrentUsername())) { throw new SecurityException(Status.KICKOUT_SELF); } monitorService.kickout(names); return ApiResponse.ofSuccess(); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/TestController.java ================================================ package com.xkcoding.rbac.security.controller; import com.xkcoding.rbac.security.common.ApiResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; /** *

    * 测试Controller *

    * * @author yangkai.shen * @date Created in 2018-12-10 15:44 */ @Slf4j @RestController @RequestMapping("/test") public class TestController { @GetMapping public ApiResponse list() { log.info("测试列表查询"); return ApiResponse.ofMessage("测试列表查询"); } @PostMapping public ApiResponse add() { log.info("测试列表添加"); return ApiResponse.ofMessage("测试列表添加"); } @PutMapping("/{id}") public ApiResponse update(@PathVariable Long id) { log.info("测试列表修改"); return ApiResponse.ofSuccess("测试列表修改"); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java ================================================ package com.xkcoding.rbac.security.exception; import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.Status; import lombok.Data; import lombok.EqualsAndHashCode; /** *

    * 全局异常 *

    * * @author yangkai.shen * @date Created in 2018-12-10 17:24 */ @EqualsAndHashCode(callSuper = true) @Data public class SecurityException extends BaseException { public SecurityException(Status status) { super(status); } public SecurityException(Status status, Object data) { super(status, data); } public SecurityException(Integer code, String message) { super(code, message); } public SecurityException(Integer code, String message, Object data) { super(code, message, data); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java ================================================ package com.xkcoding.rbac.security.exception.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.json.JSONUtil; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.Status; import lombok.extern.slf4j.Slf4j; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; 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.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; import javax.validation.ConstraintViolationException; /** *

    * 全局统一异常处理 *

    * * @author yangkai.shen * @date Created in 2018-12-10 17:00 */ @ControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(value = Exception.class) @ResponseBody public ApiResponse handlerException(Exception e) { if (e instanceof NoHandlerFoundException) { log.error("【全局异常拦截】NoHandlerFoundException: 请求方法 {}, 请求路径 {}", ((NoHandlerFoundException) e).getRequestURL(), ((NoHandlerFoundException) e).getHttpMethod()); return ApiResponse.ofStatus(Status.REQUEST_NOT_FOUND); } else if (e instanceof HttpRequestMethodNotSupportedException) { log.error("【全局异常拦截】HttpRequestMethodNotSupportedException: 当前请求方式 {}, 支持请求方式 {}", ((HttpRequestMethodNotSupportedException) e).getMethod(), JSONUtil.toJsonStr(((HttpRequestMethodNotSupportedException) e).getSupportedHttpMethods())); return ApiResponse.ofStatus(Status.HTTP_BAD_METHOD); } else if (e instanceof MethodArgumentNotValidException) { log.error("【全局异常拦截】MethodArgumentNotValidException", e); return ApiResponse.of(Status.BAD_REQUEST.getCode(), ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().get(0).getDefaultMessage(), null); } else if (e instanceof ConstraintViolationException) { log.error("【全局异常拦截】ConstraintViolationException", e); return ApiResponse.of(Status.BAD_REQUEST.getCode(), CollUtil.getFirst(((ConstraintViolationException) e).getConstraintViolations()).getMessage(), null); } else if (e instanceof MethodArgumentTypeMismatchException) { log.error("【全局异常拦截】MethodArgumentTypeMismatchException: 参数名 {}, 异常信息 {}", ((MethodArgumentTypeMismatchException) e).getName(), ((MethodArgumentTypeMismatchException) e).getMessage()); return ApiResponse.ofStatus(Status.PARAM_NOT_MATCH); } else if (e instanceof HttpMessageNotReadableException) { log.error("【全局异常拦截】HttpMessageNotReadableException: 错误信息 {}", ((HttpMessageNotReadableException) e).getMessage()); return ApiResponse.ofStatus(Status.PARAM_NOT_NULL); } else if (e instanceof BadCredentialsException) { log.error("【全局异常拦截】BadCredentialsException: 错误信息 {}", e.getMessage()); return ApiResponse.ofStatus(Status.USERNAME_PASSWORD_ERROR); } else if (e instanceof DisabledException) { log.error("【全局异常拦截】BadCredentialsException: 错误信息 {}", e.getMessage()); return ApiResponse.ofStatus(Status.USER_DISABLED); } else if (e instanceof BaseException) { log.error("【全局异常拦截】DataManagerException: 状态码 {}, 异常信息 {}", ((BaseException) e).getCode(), e.getMessage()); return ApiResponse.ofException((BaseException) e); } log.error("【全局异常拦截】: 异常信息 {} ", e.getMessage()); return ApiResponse.ofStatus(Status.ERROR); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/Permission.java ================================================ package com.xkcoding.rbac.security.model; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; /** *

    * 权限 *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:04 */ @Data @Entity @Table(name = "sec_permission") public class Permission { /** * 主键 */ @Id private Long id; /** * 权限名 */ private String name; /** * 类型为页面时,代表前端路由地址,类型为按钮时,代表后端接口地址 */ private String url; /** * 权限类型,页面-1,按钮-2 */ private Integer type; /** * 权限表达式 */ private String permission; /** * 后端接口访问方式 */ private String method; /** * 排序 */ private Integer sort; /** * 父级id */ @Column(name = "parent_id") private Long parentId; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/Role.java ================================================ package com.xkcoding.rbac.security.model; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; /** *

    * 角色 *

    * * @author yangkai.shen * @date Created in 2018-12-07 15:45 */ @Data @Entity @Table(name = "sec_role") public class Role { /** * 主键 */ @Id private Long id; /** * 角色名 */ private String name; /** * 描述 */ private String description; /** * 创建时间 */ @Column(name = "create_time") private Long createTime; /** * 更新时间 */ @Column(name = "update_time") private Long updateTime; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/RolePermission.java ================================================ package com.xkcoding.rbac.security.model; import com.xkcoding.rbac.security.model.unionkey.RolePermissionKey; import lombok.Data; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Table; /** *

    * 角色-权限 *

    * * @author yangkai.shen * @date Created in 2018-12-10 13:46 */ @Data @Entity @Table(name = "sec_role_permission") public class RolePermission { /** * 主键 */ @EmbeddedId private RolePermissionKey id; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/User.java ================================================ package com.xkcoding.rbac.security.model; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; /** *

    * 用户 *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:00 */ @Data @Entity @Table(name = "sec_user") public class User { /** * 主键 */ @Id private Long id; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 昵称 */ private String nickname; /** * 手机 */ private String phone; /** * 邮箱 */ private String email; /** * 生日 */ private Long birthday; /** * 性别,男-1,女-2 */ private Integer sex; /** * 状态,启用-1,禁用-0 */ private Integer status; /** * 创建时间 */ @Column(name = "create_time") private Long createTime; /** * 更新时间 */ @Column(name = "update_time") private Long updateTime; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/UserRole.java ================================================ package com.xkcoding.rbac.security.model; import com.xkcoding.rbac.security.model.unionkey.UserRoleKey; import lombok.Data; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Table; /** *

    * 用户角色关联 *

    * * @author yangkai.shen * @date Created in 2018-12-10 11:18 */ @Data @Entity @Table(name = "sec_user_role") public class UserRole { /** * 主键 */ @EmbeddedId private UserRoleKey id; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/unionkey/RolePermissionKey.java ================================================ package com.xkcoding.rbac.security.model.unionkey; import lombok.Data; import javax.persistence.Column; import javax.persistence.Embeddable; import java.io.Serializable; /** *

    * 角色-权限联合主键 *

    * * @author yangkai.shen * @date Created in 2018-12-10 13:47 */ @Data @Embeddable public class RolePermissionKey implements Serializable { private static final long serialVersionUID = 6850974328279713855L; /** * 角色id */ @Column(name = "role_id") private Long roleId; /** * 权限id */ @Column(name = "permission_id") private Long permissionId; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/model/unionkey/UserRoleKey.java ================================================ package com.xkcoding.rbac.security.model.unionkey; import lombok.Data; import javax.persistence.Column; import javax.persistence.Embeddable; import java.io.Serializable; /** *

    * 用户-角色联合主键 *

    * * @author yangkai.shen * @date Created in 2018-12-10 11:20 */ @Embeddable @Data public class UserRoleKey implements Serializable { private static final long serialVersionUID = 5633412144183654743L; /** * 用户id */ @Column(name = "user_id") private Long userId; /** * 角色id */ @Column(name = "role_id") private Long roleId; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java ================================================ package com.xkcoding.rbac.security.payload; import lombok.Data; import javax.validation.constraints.NotBlank; /** *

    * 登录请求参数 *

    * * @author yangkai.shen * @date Created in 2018-12-10 15:52 */ @Data public class LoginRequest { /** * 用户名或邮箱或手机号 */ @NotBlank(message = "用户名不能为空") private String usernameOrEmailOrPhone; /** * 密码 */ @NotBlank(message = "密码不能为空") private String password; /** * 记住我 */ private Boolean rememberMe = false; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/PageCondition.java ================================================ package com.xkcoding.rbac.security.payload; import lombok.Data; /** *

    * 分页请求参数 *

    * * @author yangkai.shen * @date Created in 2018-12-12 18:05 */ @Data public class PageCondition { /** * 当前页码 */ private Integer currentPage; /** * 每页条数 */ private Integer pageSize; } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/PermissionDao.java ================================================ package com.xkcoding.rbac.security.repository; import com.xkcoding.rbac.security.model.Permission; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; /** *

    * 权限 DAO *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:21 */ public interface PermissionDao extends JpaRepository, JpaSpecificationExecutor { /** * 根据角色列表查询权限列表 * * @param ids 角色id列表 * @return 权限列表 */ @Query(value = "SELECT DISTINCT sec_permission.* FROM sec_permission,sec_role,sec_role_permission WHERE sec_role.id = sec_role_permission.role_id AND sec_permission.id = sec_role_permission.permission_id AND sec_role.id IN (:ids)", nativeQuery = true) List selectByRoleIdList(@Param("ids") List ids); } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/RoleDao.java ================================================ package com.xkcoding.rbac.security.repository; import com.xkcoding.rbac.security.model.Role; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; /** *

    * 角色 DAO *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:20 */ public interface RoleDao extends JpaRepository, JpaSpecificationExecutor { /** * 根据用户id 查询角色列表 * * @param userId 用户id * @return 角色列表 */ @Query(value = "SELECT sec_role.* FROM sec_role,sec_user,sec_user_role WHERE sec_user.id = sec_user_role.user_id AND sec_role.id = sec_user_role.role_id AND sec_user.id = :userId", nativeQuery = true) List selectByUserId(@Param("userId") Long userId); } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/RolePermissionDao.java ================================================ package com.xkcoding.rbac.security.repository; import com.xkcoding.rbac.security.model.RolePermission; import com.xkcoding.rbac.security.model.unionkey.RolePermissionKey; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; /** *

    * 角色-权限 DAO *

    * * @author yangkai.shen * @date Created in 2018-12-10 13:45 */ public interface RolePermissionDao extends JpaRepository, JpaSpecificationExecutor { } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/UserDao.java ================================================ package com.xkcoding.rbac.security.repository; import com.xkcoding.rbac.security.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import java.util.List; import java.util.Optional; /** *

    * 用户 DAO *

    * * @author yangkai.shen * @date Created in 2018-12-07 16:18 */ public interface UserDao extends JpaRepository, JpaSpecificationExecutor { /** * 根据用户名、邮箱、手机号查询用户 * * @param username 用户名 * @param email 邮箱 * @param phone 手机号 * @return 用户信息 */ Optional findByUsernameOrEmailOrPhone(String username, String email, String phone); /** * 根据用户名列表查询用户列表 * * @param usernameList 用户名列表 * @return 用户列表 */ List findByUsernameIn(List usernameList); } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/repository/UserRoleDao.java ================================================ package com.xkcoding.rbac.security.repository; import com.xkcoding.rbac.security.model.UserRole; import com.xkcoding.rbac.security.model.unionkey.UserRoleKey; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; /** *

    * 用户角色 DAO *

    * * @author yangkai.shen * @date Created in 2018-12-10 11:24 */ public interface UserRoleDao extends JpaRepository, JpaSpecificationExecutor { } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/CustomUserDetailsService.java ================================================ package com.xkcoding.rbac.security.service; import com.xkcoding.rbac.security.model.Permission; import com.xkcoding.rbac.security.model.Role; import com.xkcoding.rbac.security.model.User; import com.xkcoding.rbac.security.repository.PermissionDao; import com.xkcoding.rbac.security.repository.RoleDao; import com.xkcoding.rbac.security.repository.UserDao; import com.xkcoding.rbac.security.vo.UserPrincipal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; /** *

    * 自定义UserDetails查询 *

    * * @author yangkai.shen * @date Created in 2018-12-10 10:29 */ @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Override public UserDetails loadUserByUsername(String usernameOrEmailOrPhone) throws UsernameNotFoundException { User user = userDao.findByUsernameOrEmailOrPhone(usernameOrEmailOrPhone, usernameOrEmailOrPhone, usernameOrEmailOrPhone).orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + usernameOrEmailOrPhone)); List roles = roleDao.selectByUserId(user.getId()); List roleIds = roles.stream().map(Role::getId).collect(Collectors.toList()); List permissions = permissionDao.selectByRoleIdList(roleIds); return UserPrincipal.create(user, roles, permissions); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/MonitorService.java ================================================ package com.xkcoding.rbac.security.service; import cn.hutool.core.util.StrUtil; import com.google.common.collect.Lists; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.PageResult; import com.xkcoding.rbac.security.model.User; import com.xkcoding.rbac.security.payload.PageCondition; import com.xkcoding.rbac.security.repository.UserDao; import com.xkcoding.rbac.security.util.RedisUtil; import com.xkcoding.rbac.security.util.SecurityUtil; import com.xkcoding.rbac.security.vo.OnlineUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; /** *

    * 监控 Service *

    * * @author yangkai.shen * @date Created in 2018-12-12 00:55 */ @Slf4j @Service public class MonitorService { @Autowired private RedisUtil redisUtil; @Autowired private UserDao userDao; /** * 在线用户分页列表 * * @param pageCondition 分页参数 * @return 在线用户分页列表 */ public PageResult onlineUser(PageCondition pageCondition) { PageResult keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, pageCondition.getCurrentPage(), pageCondition.getPageSize()); List rows = keys.getRows(); Long total = keys.getTotal(); // 根据 redis 中键获取用户名列表 List usernameList = rows.stream().map(s -> StrUtil.subAfter(s, Consts.REDIS_JWT_KEY_PREFIX, true)).collect(Collectors.toList()); // 根据用户名查询用户信息 List userList = userDao.findByUsernameIn(usernameList); // 封装在线用户信息 List onlineUserList = Lists.newArrayList(); userList.forEach(user -> onlineUserList.add(OnlineUser.create(user))); return new PageResult<>(onlineUserList, total); } /** * 踢出在线用户 * * @param names 用户名列表 */ public void kickout(List names) { // 清除 Redis 中的 JWT 信息 List redisKeys = names.parallelStream().map(s -> Consts.REDIS_JWT_KEY_PREFIX + s).collect(Collectors.toList()); redisUtil.delete(redisKeys); // 获取当前用户名 String currentUsername = SecurityUtil.getCurrentUsername(); names.parallelStream().forEach(name -> { // TODO: 通知被踢出的用户已被当前登录用户踢出, // 后期考虑使用 websocket 实现,具体伪代码实现如下。 // String message = "您已被用户【" + currentUsername + "】手动下线!"; log.debug("用户【{}】被用户【{}】手动下线!", name, currentUsername); }); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java ================================================ package com.xkcoding.rbac.security.util; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.config.JwtConfig; import com.xkcoding.rbac.security.exception.SecurityException; import com.xkcoding.rbac.security.vo.UserPrincipal; import io.jsonwebtoken.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; /** *

    * JWT 工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-07 13:42 */ @EnableConfigurationProperties(JwtConfig.class) @Configuration @Slf4j public class JwtUtil { @Autowired private JwtConfig jwtConfig; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 创建JWT * * @param rememberMe 记住我 * @param id 用户id * @param subject 用户名 * @param roles 用户角色 * @param authorities 用户权限 * @return JWT */ public String createJWT(Boolean rememberMe, Long id, String subject, List roles, Collection authorities) { Date now = new Date(); JwtBuilder builder = Jwts.builder().setId(id.toString()).setSubject(subject).setIssuedAt(now).signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()).claim("roles", roles).claim("authorities", authorities); // 设置过期时间 Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl(); if (ttl > 0) { builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue())); } String jwt = builder.compact(); // 将生成的JWT保存至Redis stringRedisTemplate.opsForValue().set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS); return jwt; } /** * 创建JWT * * @param authentication 用户认证信息 * @param rememberMe 记住我 * @return JWT */ public String createJWT(Authentication authentication, Boolean rememberMe) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities()); } /** * 解析JWT * * @param jwt JWT * @return {@link Claims} */ public Claims parseJWT(String jwt) { try { Claims claims = Jwts.parser().setSigningKey(jwtConfig.getKey()).parseClaimsJws(jwt).getBody(); String username = claims.getSubject(); String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username; // 校验redis中的JWT是否存在 Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); if (Objects.isNull(expire) || expire <= 0) { throw new SecurityException(Status.TOKEN_EXPIRED); } // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期 String redisToken = stringRedisTemplate.opsForValue().get(redisKey); if (!StrUtil.equals(jwt, redisToken)) { throw new SecurityException(Status.TOKEN_OUT_OF_CTRL); } return claims; } catch (ExpiredJwtException e) { log.error("Token 已过期"); throw new SecurityException(Status.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { log.error("不支持的 Token"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } catch (MalformedJwtException e) { log.error("Token 无效"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } catch (SignatureException e) { log.error("无效的 Token 签名"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } catch (IllegalArgumentException e) { log.error("Token 参数不存在"); throw new SecurityException(Status.TOKEN_PARSE_ERROR); } } /** * 设置JWT过期 * * @param request 请求 */ public void invalidateJWT(HttpServletRequest request) { String jwt = getJwtFromRequest(request); String username = getUsernameFromJWT(jwt); // 从redis中清除JWT stringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username); } /** * 根据 jwt 获取用户名 * * @param jwt JWT * @return 用户名 */ public String getUsernameFromJWT(String jwt) { Claims claims = parseJWT(jwt); return claims.getSubject(); } /** * 从 request 的 header 中获取 JWT * * @param request 请求 * @return JWT */ public String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/PageUtil.java ================================================ package com.xkcoding.rbac.security.util; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.payload.PageCondition; import org.springframework.data.domain.PageRequest; /** *

    * 分页工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-12 18:09 */ public class PageUtil { /** * 校验分页参数,为NULL,设置分页参数默认值 * * @param condition 查询参数 * @param clazz 类 * @param {@link PageCondition} */ public static void checkPageCondition(T condition, Class clazz) { if (ObjectUtil.isNull(condition)) { condition = ReflectUtil.newInstance(clazz); } // 校验分页参数 if (ObjectUtil.isNull(condition.getCurrentPage())) { condition.setCurrentPage(Consts.DEFAULT_CURRENT_PAGE); } if (ObjectUtil.isNull(condition.getPageSize())) { condition.setPageSize(Consts.DEFAULT_PAGE_SIZE); } } /** * 根据分页参数构建{@link PageRequest} * * @param condition 查询参数 * @param {@link PageCondition} * @return {@link PageRequest} */ public static PageRequest ofPageRequest(T condition) { return PageRequest.of(condition.getCurrentPage(), condition.getPageSize()); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java ================================================ package com.xkcoding.rbac.security.util; import com.google.common.collect.Lists; import com.xkcoding.rbac.security.common.PageResult; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisConnectionUtils; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.List; /** *

    * Redis工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-11 20:24 */ @Component @Slf4j public class RedisUtil { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率 * * @param patternKey key格式 * @param currentPage 当前页码 * @param pageSize 每页条数 * @return 分页获取指定格式key */ public PageResult findKeysForPage(String patternKey, int currentPage, int pageSize) { ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory(); RedisConnection rc = factory.getConnection(); Cursor cursor = rc.scan(options); List result = Lists.newArrayList(); long tmpIndex = 0; int startIndex = (currentPage - 1) * pageSize; int end = currentPage * pageSize; while (cursor.hasNext()) { String key = new String(cursor.next()); if (tmpIndex >= startIndex && tmpIndex < end) { result.add(key); } tmpIndex++; } try { cursor.close(); RedisConnectionUtils.releaseConnection(rc, factory); } catch (Exception e) { log.warn("Redis连接关闭异常,", e); } return new PageResult<>(result, tmpIndex); } /** * 删除 Redis 中的某个key * * @param key 键 */ public void delete(String key) { stringRedisTemplate.delete(key); } /** * 批量删除 Redis 中的某些key * * @param keys 键列表 */ public void delete(Collection keys) { stringRedisTemplate.delete(keys); } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java ================================================ package com.xkcoding.rbac.security.util; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.IStatus; import lombok.extern.slf4j.Slf4j; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** *

    * Response 通用工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-07 17:37 */ @Slf4j public class ResponseUtil { /** * 往 response 写出 json * * @param response 响应 * @param status 状态 * @param data 返回数据 */ public static void renderJson(HttpServletResponse response, IStatus status, Object data) { try { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "*"); response.setContentType("application/json;charset=UTF-8"); response.setStatus(200); // FIXME: hutool 的 BUG:JSONUtil.toJsonStr() // 将JSON转为String的时候,忽略null值的时候转成的String存在错误 response.getWriter().write(JSONUtil.toJsonStr(new JSONObject(ApiResponse.ofStatus(status, data), false))); } catch (IOException e) { log.error("Response写出JSON异常,", e); } } /** * 往 response 写出 json * * @param response 响应 * @param exception 异常 */ public static void renderJson(HttpServletResponse response, BaseException exception) { try { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "*"); response.setContentType("application/json;charset=UTF-8"); response.setStatus(200); // FIXME: hutool 的 BUG:JSONUtil.toJsonStr() // 将JSON转为String的时候,忽略null值的时候转成的String存在错误 response.getWriter().write(JSONUtil.toJsonStr(new JSONObject(ApiResponse.ofException(exception), false))); } catch (IOException e) { log.error("Response写出JSON异常,", e); } } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/SecurityUtil.java ================================================ package com.xkcoding.rbac.security.util; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.vo.UserPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; /** *

    * Spring Security工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-12 18:30 */ public class SecurityUtil { /** * 获取当前登录用户用户名 * * @return 当前登录用户用户名 */ public static String getCurrentUsername() { UserPrincipal currentUser = getCurrentUser(); return ObjectUtil.isNull(currentUser) ? Consts.ANONYMOUS_NAME : currentUser.getUsername(); } /** * 获取当前登录用户信息 * * @return 当前登录用户信息,匿名登录时,为null */ public static UserPrincipal getCurrentUser() { Object userInfo = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (userInfo instanceof UserDetails) { return (UserPrincipal) userInfo; } return null; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/JwtResponse.java ================================================ package com.xkcoding.rbac.security.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** *

    * JWT 响应返回 *

    * * @author yangkai.shen * @date Created in 2018-12-10 16:01 */ @Data @NoArgsConstructor @AllArgsConstructor public class JwtResponse { /** * token 字段 */ private String token; /** * token类型 */ private String tokenType = "Bearer"; public JwtResponse(String token) { this.token = token; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/OnlineUser.java ================================================ package com.xkcoding.rbac.security.vo; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.model.User; import lombok.Data; /** *

    * 在线用户 VO *

    * * @author yangkai.shen * @date Created in 2018-12-12 00:58 */ @Data public class OnlineUser { /** * 主键 */ private Long id; /** * 用户名 */ private String username; /** * 昵称 */ private String nickname; /** * 手机 */ private String phone; /** * 邮箱 */ private String email; /** * 生日 */ private Long birthday; /** * 性别,男-1,女-2 */ private Integer sex; public static OnlineUser create(User user) { OnlineUser onlineUser = new OnlineUser(); BeanUtil.copyProperties(user, onlineUser); // 脱敏 onlineUser.setPhone(StrUtil.hide(user.getPhone(), 3, 7)); onlineUser.setEmail(StrUtil.hide(user.getEmail(), 1, StrUtil.indexOfIgnoreCase(user.getEmail(), Consts.SYMBOL_EMAIL))); return onlineUser; } } ================================================ FILE: demo-rbac-security/src/main/java/com/xkcoding/rbac/security/vo/UserPrincipal.java ================================================ package com.xkcoding.rbac.security.vo; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.annotation.JsonIgnore; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.model.Permission; import com.xkcoding.rbac.security.model.Role; import com.xkcoding.rbac.security.model.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** *

    * 自定义User *

    * * @author yangkai.shen * @date Created in 2018-12-10 15:09 */ @Data @NoArgsConstructor @AllArgsConstructor public class UserPrincipal implements UserDetails { /** * 主键 */ private Long id; /** * 用户名 */ private String username; /** * 密码 */ @JsonIgnore private String password; /** * 昵称 */ private String nickname; /** * 手机 */ private String phone; /** * 邮箱 */ private String email; /** * 生日 */ private Long birthday; /** * 性别,男-1,女-2 */ private Integer sex; /** * 状态,启用-1,禁用-0 */ private Integer status; /** * 创建时间 */ private Long createTime; /** * 更新时间 */ private Long updateTime; /** * 用户角色列表 */ private List roles; /** * 用户权限列表 */ private Collection authorities; public static UserPrincipal create(User user, List roles, List permissions) { List roleNames = roles.stream().map(Role::getName).collect(Collectors.toList()); List authorities = permissions.stream().filter(permission -> StrUtil.isNotBlank(permission.getPermission())).map(permission -> new SimpleGrantedAuthority(permission.getPermission())).collect(Collectors.toList()); return new UserPrincipal(user.getId(), user.getUsername(), user.getPassword(), user.getNickname(), user.getPhone(), user.getEmail(), user.getBirthday(), user.getSex(), user.getStatus(), user.getCreateTime(), user.getUpdateTime(), roleNames, authorities); } @Override public Collection getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return Objects.equals(this.status, Consts.ENABLE); } } ================================================ FILE: demo-rbac-security/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: datasource: hikari: username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver 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 jpa: show-sql: true generate-ddl: false hibernate: ddl-auto: validate open-in-view: true properties: hibernate: dialect: org.hibernate.dialect.MySQL57InnoDBDialect resources: add-mappings: false mvc: throw-exception-if-no-handler-found: true redis: host: localhost port: 6379 # 连接超时时间(记得添加单位,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 jwt: config: key: xkcoding ttl: 600000 remember: 604800000 logging: level: com.xkcoding.rbac.security: debug custom: config: ignores: # 需要过滤的 post 请求 post: - "/api/auth/login" - "/api/auth/logout" # 需要过滤的请求,不限方法 pattern: - "/test/*" ================================================ FILE: demo-rbac-security/src/test/java/com/xkcoding/rbac/security/SpringBootDemoRbacSecurityApplicationTests.java ================================================ package com.xkcoding.rbac.security; 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 SpringBootDemoRbacSecurityApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-rbac-security/src/test/java/com/xkcoding/rbac/security/repository/DataInitTest.java ================================================ package com.xkcoding.rbac.security.repository; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Snowflake; import com.xkcoding.rbac.security.SpringBootDemoRbacSecurityApplicationTests; import com.xkcoding.rbac.security.model.*; import com.xkcoding.rbac.security.model.unionkey.RolePermissionKey; import com.xkcoding.rbac.security.model.unionkey.UserRoleKey; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** *

    * 数据初始化测试 *

    * * @author yangkai.shen * @date Created in 2018-12-10 11:26 */ public class DataInitTest extends SpringBootDemoRbacSecurityApplicationTests { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Autowired private UserRoleDao userRoleDao; @Autowired private RolePermissionDao rolePermissionDao; @Autowired private Snowflake snowflake; @Autowired private BCryptPasswordEncoder encoder; @Test public void initTest() { init(); } private void init() { User admin = createUser(true); User user = createUser(false); Role roleAdmin = createRole(true); Role roleUser = createRole(false); createUserRoleRelation(admin.getId(), roleAdmin.getId()); createUserRoleRelation(user.getId(), roleUser.getId()); // 页面权限 Permission testPagePerm = createPermission("/test", "测试页面", 1, "page:test", null, 1, 0L); // 按钮权限 Permission testBtnQueryPerm = createPermission("/**/test", "测试页面-查询", 2, "btn:test:query", "GET", 1, testPagePerm.getId()); Permission testBtnPermInsert = createPermission("/**/test", "测试页面-添加", 2, "btn:test:insert", "POST", 2, testPagePerm.getId()); Permission monitorOnlinePagePerm = createPermission("/monitor", "监控在线用户页面", 1, "page:monitor:online", null, 2, 0L); Permission monitorOnlineBtnQueryPerm = createPermission("/**/api/monitor/online/user", "在线用户页面-查询", 2, "btn:monitor:online:query", "GET", 1, monitorOnlinePagePerm.getId()); Permission monitorOnlineBtnKickoutPerm = createPermission("/**/api/monitor/online/user/kickout", "在线用户页面-踢出", 2, "btn:monitor:online:kickout", "DELETE", 2, monitorOnlinePagePerm.getId()); createRolePermissionRelation(roleAdmin.getId(), testPagePerm.getId()); createRolePermissionRelation(roleUser.getId(), testPagePerm.getId()); createRolePermissionRelation(roleAdmin.getId(), testBtnQueryPerm.getId()); createRolePermissionRelation(roleUser.getId(), testBtnQueryPerm.getId()); createRolePermissionRelation(roleAdmin.getId(), testBtnPermInsert.getId()); createRolePermissionRelation(roleAdmin.getId(), monitorOnlinePagePerm.getId()); createRolePermissionRelation(roleAdmin.getId(), monitorOnlineBtnQueryPerm.getId()); createRolePermissionRelation(roleAdmin.getId(), monitorOnlineBtnKickoutPerm.getId()); } private void createRolePermissionRelation(Long roleId, Long permissionId) { RolePermission adminPage = new RolePermission(); RolePermissionKey adminPageKey = new RolePermissionKey(); adminPageKey.setRoleId(roleId); adminPageKey.setPermissionId(permissionId); adminPage.setId(adminPageKey); rolePermissionDao.save(adminPage); } private Permission createPermission(String url, String name, Integer type, String permission, String method, Integer sort, Long parentId) { Permission perm = new Permission(); perm.setId(snowflake.nextId()); perm.setUrl(url); perm.setName(name); perm.setType(type); perm.setPermission(permission); perm.setMethod(method); perm.setSort(sort); perm.setParentId(parentId); permissionDao.save(perm); return perm; } private void createUserRoleRelation(Long userId, Long roleId) { UserRole userRole = new UserRole(); UserRoleKey key = new UserRoleKey(); key.setUserId(userId); key.setRoleId(roleId); userRole.setId(key); userRoleDao.save(userRole); } private Role createRole(boolean isAdmin) { Role role = new Role(); role.setId(snowflake.nextId()); role.setName(isAdmin ? "管理员" : "普通用户"); role.setDescription(isAdmin ? "超级管理员" : "普通用户"); role.setCreateTime(DateUtil.current(false)); role.setUpdateTime(DateUtil.current(false)); roleDao.save(role); return role; } private User createUser(boolean isAdmin) { User user = new User(); user.setId(snowflake.nextId()); user.setUsername(isAdmin ? "admin" : "user"); user.setNickname(isAdmin ? "管理员" : "普通用户"); user.setPassword(encoder.encode("123456")); user.setBirthday(DateTime.of("1994-11-22", "yyyy-MM-dd").getTime()); user.setEmail((isAdmin ? "admin" : "user") + "@xkcoding.com"); user.setPhone(isAdmin ? "17300000000" : "17300001111"); user.setSex(1); user.setStatus(1); user.setCreateTime(DateUtil.current(false)); user.setUpdateTime(DateUtil.current(false)); userDao.save(user); return user; } } ================================================ FILE: demo-rbac-security/src/test/java/com/xkcoding/rbac/security/repository/UserDaoTest.java ================================================ package com.xkcoding.rbac.security.repository; import com.xkcoding.rbac.security.SpringBootDemoRbacSecurityApplicationTests; import com.xkcoding.rbac.security.model.User; import lombok.extern.slf4j.Slf4j; import org.assertj.core.util.Lists; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** *

    * UserDao 测试 *

    * * @author yangkai.shen * @date Created in 2018-12-12 01:10 */ @Slf4j public class UserDaoTest extends SpringBootDemoRbacSecurityApplicationTests { @Autowired private UserDao userDao; @Test public void findByUsernameIn() { List usernameList = Lists.newArrayList("admin", "user"); List userList = userDao.findByUsernameIn(usernameList); Assert.assertEquals(2, userList.size()); log.info("【userList】= {}", userList); } } ================================================ FILE: demo-rbac-security/src/test/java/com/xkcoding/rbac/security/util/RedisUtilTest.java ================================================ package com.xkcoding.rbac.security.util; import cn.hutool.json.JSONUtil; import com.xkcoding.rbac.security.SpringBootDemoRbacSecurityApplicationTests; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.PageResult; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; /** *

    * 测试RedisUtil *

    * * @author yangkai.shen * @date Created in 2018-12-11 20:44 */ @Slf4j public class RedisUtilTest extends SpringBootDemoRbacSecurityApplicationTests { @Autowired private RedisUtil redisUtil; @Test public void findKeysForPage() { PageResult pageResult = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, 2, 1); log.info("【pageResult】= {}", JSONUtil.toJsonStr(pageResult)); } } ================================================ FILE: demo-rbac-shiro/.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-rbac-shiro/pom.xml ================================================ 4.0.0 demo-rbac-shiro 1.0.0-SNAPSHOT jar demo-rbac-shiro 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.baomidou mybatis-plus-boot-starter 3.1.0 p6spy p6spy 3.8.1 org.apache.shiro shiro-spring-boot-starter 1.4.0 mysql mysql-connector-java cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-rbac-shiro org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-rbac-shiro/sql/shiro.sql ================================================ /* Navicat Premium Data Transfer Source Server : 本机 Source Server Type : MySQL Source Server Version : 50718 Source Host : localhost:3306 Source Schema : spring-boot-demo Target Server Type : MySQL Target Server Version : 50718 File Encoding : 65001 Date: 12/12/2018 18:52:51 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sec_user -- ---------------------------- DROP TABLE IF EXISTS `shiro_user`; CREATE TABLE `shiro_user` ( `id` bigint(64) NOT NULL COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(60) NOT NULL COMMENT '密码', `salt` varchar(60) NOT NULL COMMENT '盐值', `nickname` varchar(255) DEFAULT NULL COMMENT '昵称', `phone` varchar(11) DEFAULT NULL COMMENT '手机', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `birthday` bigint(13) DEFAULT NULL COMMENT '生日', `sex` int(2) DEFAULT NULL COMMENT '性别,男-1,女-2', `status` int(2) NOT NULL DEFAULT '1' COMMENT '状态,启用-1,禁用-0', `create_time` bigint(13) NOT NULL COMMENT '创建时间', `update_time` bigint(13) NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`), UNIQUE KEY `phone` (`phone`), UNIQUE KEY `email` (`email`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='用户表'; -- ---------------------------- -- Table structure for sec_role -- ---------------------------- DROP TABLE IF EXISTS `shiro_role`; CREATE TABLE `shiro_role` ( `id` bigint(64) NOT NULL COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '角色名', `description` varchar(100) DEFAULT NULL COMMENT '描述', `create_time` bigint(13) NOT NULL COMMENT '创建时间', `update_time` bigint(13) NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='角色表'; -- ---------------------------- -- Table structure for sec_user_role -- ---------------------------- DROP TABLE IF EXISTS `shiro_user_role`; CREATE TABLE `shiro_user_role` ( `user_id` bigint(64) NOT NULL COMMENT '用户主键', `role_id` bigint(64) NOT NULL COMMENT '角色主键', PRIMARY KEY (`user_id`, `role_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='用户角色关系表'; -- ---------------------------- -- Table structure for sec_permission -- ---------------------------- DROP TABLE IF EXISTS `shiro_permission`; CREATE TABLE `shiro_permission` ( `id` bigint(64) NOT NULL COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '权限名', `url` varchar(1000) DEFAULT NULL COMMENT '类型为页面时,代表前端路由地址,类型为按钮时,代表后端接口地址', `type` int(2) NOT NULL COMMENT '权限类型,页面-1,按钮-2', `permission` varchar(50) DEFAULT NULL COMMENT '权限表达式', `method` varchar(50) DEFAULT NULL COMMENT '后端接口访问方式', `sort` int(11) NOT NULL COMMENT '排序', `parent_id` bigint(64) NOT NULL COMMENT '父级id', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='权限表'; -- ---------------------------- -- Table structure for sec_role_permission -- ---------------------------- DROP TABLE IF EXISTS `shiro_role_permission`; CREATE TABLE `shiro_role_permission` ( `role_id` bigint(64) NOT NULL COMMENT '角色主键', `permission_id` bigint(64) NOT NULL COMMENT '权限主键', PRIMARY KEY (`role_id`, `permission_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='角色权限关系表'; ================================================ FILE: demo-rbac-shiro/src/main/java/com/xkcoding/rbac/shiro/SpringBootDemoRbacShiroApplication.java ================================================ package com.xkcoding.rbac.shiro; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-03-21 16:11 */ @SpringBootApplication @MapperScan("com.xkcoding.rbac.shiro.mapper") public class SpringBootDemoRbacShiroApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoRbacShiroApplication.class, args); } } ================================================ FILE: demo-rbac-shiro/src/main/java/com/xkcoding/rbac/shiro/common/IResultCode.java ================================================ package com.xkcoding.rbac.shiro.common; /** *

    * 统一状态码接口 *

    * * @author yangkai.shen * @date Created in 2019-03-21 16:28 */ public interface IResultCode { /** * 获取状态码 * * @return 状态码 */ Integer getCode(); /** * 获取返回消息 * * @return 返回消息 */ String getMessage(); } ================================================ FILE: demo-rbac-shiro/src/main/java/com/xkcoding/rbac/shiro/common/R.java ================================================ package com.xkcoding.rbac.shiro.common; import lombok.Data; import lombok.NoArgsConstructor; /** *

    * 统一API对象返回 *

    * * @author yangkai.shen * @date Created in 2019-03-21 16:24 */ @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-rbac-shiro/src/main/java/com/xkcoding/rbac/shiro/common/ResultCode.java ================================================ package com.xkcoding.rbac.shiro.common; import lombok.Getter; /** *

    * 通用状态枚举 *

    * * @author yangkai.shen * @date Created in 2019-03-21 16:31 */ @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-rbac-shiro/src/main/java/com/xkcoding/rbac/shiro/config/MybatisPlusConfig.java ================================================ package com.xkcoding.rbac.shiro.config; import com.baomidou.mybatisplus.core.parser.ISqlParser; import com.baomidou.mybatisplus.extension.parsers.BlockAttackSqlParser; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.List; /** *

    * MP3 配置 *

    * * @author yangkai.shen * @date Created in 2019-03-21 17:06 */ @Configuration public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); List sqlParserList = new ArrayList<>(); // 攻击 SQL 阻断解析器、加入解析链 sqlParserList.add(new BlockAttackSqlParser()); paginationInterceptor.setSqlParserList(sqlParserList); return paginationInterceptor; } /** * SQL执行效率插件 */ @Bean public PerformanceInterceptor performanceInterceptor() { return new PerformanceInterceptor(); } } ================================================ FILE: demo-rbac-shiro/src/main/java/com/xkcoding/rbac/shiro/controller/TestController.java ================================================ package com.xkcoding.rbac.shiro.controller; import com.xkcoding.rbac.shiro.common.R; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** *

    * 测试Controller *

    * * @author yangkai.shen * @date Created in 2019-03-21 16:13 */ @RestController @RequestMapping("/test") public class TestController { @GetMapping("") public R test() { return R.success(); } } ================================================ FILE: demo-rbac-shiro/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: datasource: hikari: username: root password: root driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8 mybatis-plus: global-config: # 关闭banner banner: false ================================================ FILE: demo-rbac-shiro/src/main/resources/spy.properties ================================================ module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql #appender=com.p6spy.engine.spy.appender.Slf4JLogger # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,batch,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2 ================================================ FILE: demo-rbac-shiro/src/test/java/com/xkcoding/rbac/shiro/SpringBootDemoRbacShiroApplicationTests.java ================================================ package com.xkcoding.rbac.shiro; 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 SpringBootDemoRbacShiroApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-session/.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-session/README.md ================================================ # spring-boot-demo-session > 此 demo 主要演示了 Spring Boot 如何通过 Spring Session 实现Session共享、重启程序Session不失效。 ## pom.xml ```xml 4.0.0 spring-boot-demo-session 1.0.0-SNAPSHOT spring-boot-demo-session 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.session spring-session-data-redis org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all spring-boot-demo-session org.springframework.boot spring-boot-maven-plugin ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo spring: session: store-type: redis redis: flush-mode: immediate namespace: "spring:session" redis: host: localhost port: 6379 # 连接超时时间(记得添加单位,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 ``` ## 测试 > 测试 重启程序,Session 不失效的场景 1. 打开浏览器,访问首页:http://localhost:8080/demo/page/index 2. 最开始未登录,所以会跳转到登录页:http://localhost:8080/demo/page/login?redirect=true 然后点击登录按钮 3. 登录之后,跳转回首页,此时可以看到首页显示token信息。 4. 重启程序。不关闭浏览器,直接刷新首页,此时不跳转到登录页。测试成功! ## 参考 - Spring Session 官方文档:https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html#updating-dependencies ================================================ FILE: demo-session/pom.xml ================================================ 4.0.0 demo-session 1.0.0-SNAPSHOT jar demo-session 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.session spring-session-data-redis org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all demo-session org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-session/src/main/java/com/xkcoding/session/SpringBootDemoSessionApplication.java ================================================ package com.xkcoding.session; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-12-19 19:35 */ @SpringBootApplication public class SpringBootDemoSessionApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoSessionApplication.class, args); } } ================================================ FILE: demo-session/src/main/java/com/xkcoding/session/config/WebMvcConfig.java ================================================ package com.xkcoding.session.config; import com.xkcoding.session.interceptor.SessionInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** *

    * WebMvc 配置类 *

    * * @author yangkai.shen * @date Created in 2018-12-19 19:50 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private SessionInterceptor sessionInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { InterceptorRegistration sessionInterceptorRegistry = registry.addInterceptor(sessionInterceptor); // 排除不需要拦截的路径 sessionInterceptorRegistry.excludePathPatterns("/page/login"); sessionInterceptorRegistry.excludePathPatterns("/page/doLogin"); sessionInterceptorRegistry.excludePathPatterns("/error"); // 需要拦截的路径 sessionInterceptorRegistry.addPathPatterns("/**"); } } ================================================ FILE: demo-session/src/main/java/com/xkcoding/session/constants/Consts.java ================================================ package com.xkcoding.session.constants; /** *

    * 常量池 *

    * * @author yangkai.shen * @date Created in 2018-12-19 19:42 */ public interface Consts { /** * session保存的key */ String SESSION_KEY = "key:session:token"; } ================================================ FILE: demo-session/src/main/java/com/xkcoding/session/controller/PageController.java ================================================ package com.xkcoding.session.controller; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.session.constants.Consts; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** *

    * 页面跳转 Controller *

    * * @author yangkai.shen * @date Created in 2018-12-19 19:57 */ @Controller @RequestMapping("/page") public class PageController { /** * 跳转到 首页 * * @param request 请求 */ @GetMapping("/index") public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); String token = (String) request.getSession().getAttribute(Consts.SESSION_KEY); mv.setViewName("index"); mv.addObject("token", token); return mv; } /** * 跳转到 登录页 * * @param redirect 是否是跳转回来的 */ @GetMapping("/login") public ModelAndView login(Boolean redirect) { ModelAndView mv = new ModelAndView(); if (ObjectUtil.isNotNull(redirect) && ObjectUtil.equal(true, redirect)) { mv.addObject("message", "请先登录!"); } mv.setViewName("login"); return mv; } @GetMapping("/doLogin") public String doLogin(HttpSession session) { session.setAttribute(Consts.SESSION_KEY, IdUtil.fastUUID()); return "redirect:/page/index"; } } ================================================ FILE: demo-session/src/main/java/com/xkcoding/session/interceptor/SessionInterceptor.java ================================================ package com.xkcoding.session.interceptor; import com.xkcoding.session.constants.Consts; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** *

    * 校验Session的拦截器 *

    * * @author yangkai.shen * @date Created in 2018-12-19 19:40 */ @Component public class SessionInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute(Consts.SESSION_KEY) != null) { return true; } // 跳转到登录页 String url = "/page/login?redirect=true"; response.sendRedirect(request.getContextPath() + url); return false; } } ================================================ FILE: demo-session/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: session: store-type: redis redis: flush-mode: immediate namespace: "spring:session" redis: host: localhost port: 6379 # 连接超时时间(记得添加单位,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 ================================================ FILE: demo-session/src/main/resources/templates/index.html ================================================ spring-boot-demo-session token的值:

    ================================================ FILE: demo-session/src/main/resources/templates/login.html ================================================ spring-boot-demo-session

    ================================================ FILE: demo-session/src/test/java/com/xkcoding/session/SpringBootDemoSessionApplicationTests.java ================================================ package com.xkcoding.session; 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 SpringBootDemoSessionApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-sharding-jdbc/.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-sharding-jdbc/README.md ================================================ # spring-boot-demo-sharding-jdbc > 本 demo 主要演示了如何集成 `sharding-jdbc` 实现分库分表操作,ORM 层使用了`Mybatis-Plus`简化开发,童鞋们可以按照自己的喜好替换为 JPA、通用Mapper、JdbcTemplate甚至原生的JDBC都可以。 > > PS: > > 1. 目前当当官方提供的starter存在bug,版本号:`3.1.0`,因此本demo采用手动配置。 > 2. 文档真的很垃圾​ :joy: ## 1. 运行方式 1. 在数据库创建2个数据库,分别为:`spring-boot-demo`、`spring-boot-demo-2` 2. 去数据库执行 `sql/schema.sql` ,创建 `6` 张分片表 3. 找到 `DataSourceShardingConfig` 配置类,修改 `数据源` 的相关配置,位于 `dataSourceMap()` 这个方法 4. 找到测试类 `SpringBootDemoShardingJdbcApplicationTests` 进行测试 ## 2. 关键代码 ### 2.1. `pom.xml` ```xml 4.0.0 spring-boot-demo-sharding-jdbc 1.0.0-SNAPSHOT jar spring-boot-demo-sharding-jdbc 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 com.baomidou mybatis-plus-boot-starter 3.1.0 mysql mysql-connector-java io.shardingsphere sharding-jdbc-core 3.1.0 cn.hutool hutool-all org.projectlombok lombok true spring-boot-demo-sharding-jdbc org.springframework.boot spring-boot-maven-plugin ``` ### 2.2. `CustomSnowflakeKeyGenerator.java` ```java package com.xkcoding.sharding.jdbc.config; import cn.hutool.core.lang.Snowflake; import io.shardingsphere.core.keygen.KeyGenerator; /** *

    * 自定义雪花算法,替换 DefaultKeyGenerator,避免DefaultKeyGenerator生成的id大几率是偶数 *

    * * @author yangkai.shen * @date Created in 2019-03-26 17:07 */ public class CustomSnowflakeKeyGenerator implements KeyGenerator { private Snowflake snowflake; public CustomSnowflakeKeyGenerator(Snowflake snowflake) { this.snowflake = snowflake; } @Override public Number generateKey() { return snowflake.nextId(); } } ``` ### 2.3. `DataSourceShardingConfig.java` ```java /** *

    * sharding-jdbc 的数据源配置 *

    * * @author yangkai.shen * @date Created in 2019-03-26 16:47 */ @Configuration public class DataSourceShardingConfig { private static final Snowflake snowflake = IdUtil.createSnowflake(1, 1); /** * 需要手动配置事务管理器 */ @Bean public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "dataSource") @Primary public DataSource dataSource() throws SQLException { ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); // 设置分库策略 shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}")); // 设置规则适配的表 shardingRuleConfig.getBindingTableGroups().add("t_order"); // 设置分表策略 shardingRuleConfig.getTableRuleConfigs().add(orderTableRule()); shardingRuleConfig.setDefaultDataSourceName("ds0"); shardingRuleConfig.setDefaultTableShardingStrategyConfig(new NoneShardingStrategyConfiguration()); Properties properties = new Properties(); properties.setProperty("sql.show", "true"); return ShardingDataSourceFactory.createDataSource(dataSourceMap(), shardingRuleConfig, new ConcurrentHashMap<>(16), properties); } private TableRuleConfiguration orderTableRule() { TableRuleConfiguration tableRule = new TableRuleConfiguration(); // 设置逻辑表名 tableRule.setLogicTable("t_order"); // ds${0..1}.t_order_${0..2} 也可以写成 ds$->{0..1}.t_order_$->{0..1} tableRule.setActualDataNodes("ds${0..1}.t_order_${0..2}"); tableRule.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order_$->{order_id % 3}")); tableRule.setKeyGenerator(customKeyGenerator()); tableRule.setKeyGeneratorColumnName("order_id"); return tableRule; } private Map dataSourceMap() { Map dataSourceMap = new HashMap<>(16); // 配置第一个数据源 HikariDataSource ds0 = new HikariDataSource(); ds0.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds0.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8"); ds0.setUsername("root"); ds0.setPassword("root"); // 配置第二个数据源 HikariDataSource ds1 = new HikariDataSource(); ds1.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds1.setJdbcUrl("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"); ds1.setUsername("root"); ds1.setPassword("root"); dataSourceMap.put("ds0", ds0); dataSourceMap.put("ds1", ds1); return dataSourceMap; } /** * 自定义主键生成器 */ private KeyGenerator customKeyGenerator() { return new CustomSnowflakeKeyGenerator(snowflake); } } ``` ### 2.3. `SpringBootDemoShardingJdbcApplicationTests.java` ```java /** *

    * 测试sharding-jdbc分库分表 *

    * * @author yangkai.shen * @date Created in 2019-03-26 13:44 */ @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class SpringBootDemoShardingJdbcApplicationTests { @Autowired private OrderMapper orderMapper; /** * 测试新增 */ @Test public void testInsert() { for (long i = 1; i < 10; i++) { for (long j = 1; j < 20; j++) { Order order = Order.builder().userId(i).orderId(j).remark(RandomUtil.randomString(20)).build(); orderMapper.insert(order); } } } /** * 测试更新 */ @Test public void testUpdate() { Order update = new Order(); update.setRemark("修改备注信息"); orderMapper.update(update, Wrappers.update().lambda().eq(Order::getOrderId, 2).eq(Order::getUserId, 2)); } /** * 测试删除 */ @Test public void testDelete() { orderMapper.delete(new QueryWrapper<>()); } /** * 测试查询 */ @Test public void testSelect() { List orders = orderMapper.selectList(Wrappers.query().lambda().in(Order::getOrderId, 1, 2)); log.info("【orders】= {}", JSONUtil.toJsonStr(orders)); } } ``` ## 3. 参考 1. `ShardingSphere` 官网:https://shardingsphere.apache.org/index_zh.html (虽然文档确实垃圾,但是还是得参考啊~) 2. `Mybatis-Plus` 语法参考官网:https://mybatis.plus/ ================================================ FILE: demo-sharding-jdbc/pom.xml ================================================ 4.0.0 demo-sharding-jdbc 1.0.0-SNAPSHOT jar demo-sharding-jdbc 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 com.baomidou mybatis-plus-boot-starter 3.1.0 mysql mysql-connector-java io.shardingsphere sharding-jdbc-core 3.1.0 cn.hutool hutool-all org.projectlombok lombok true demo-sharding-jdbc org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-sharding-jdbc/sql/schema.sql ================================================ USE `spring-boot-demo`; DROP TABLE IF EXISTS `t_order_0`; CREATE TABLE `t_order_0` ( `id` BIGINT NOT NULL COMMENT '主键', `user_id` BIGINT NOT NULL COMMENT '用户id', `order_id` BIGINT NOT NULL COMMENT '订单id', `remark` VARCHAR(200) DEFAULT '' COMMENT '备注', primary key (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='Spring Boot Demo 分库分表 系列示例表0'; DROP TABLE IF EXISTS `t_order_1`; CREATE TABLE `t_order_1` ( `id` BIGINT NOT NULL COMMENT '主键', `user_id` BIGINT NOT NULL COMMENT '用户id', `order_id` BIGINT NOT NULL COMMENT '订单id', `remark` VARCHAR(200) DEFAULT '' COMMENT '备注', primary key (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='Spring Boot Demo 分库分表 系列示例表1'; DROP TABLE IF EXISTS `t_order_2`; CREATE TABLE `t_order_2` ( `id` BIGINT NOT NULL COMMENT '主键', `user_id` BIGINT NOT NULL COMMENT '用户id', `order_id` BIGINT NOT NULL COMMENT '订单id', `remark` VARCHAR(200) DEFAULT '' COMMENT '备注', primary key (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='Spring Boot Demo 分库分表 系列示例表2'; USE `spring-boot-demo-2`; DROP TABLE IF EXISTS `t_order_0`; CREATE TABLE `t_order_0` ( `id` BIGINT NOT NULL COMMENT '主键', `user_id` BIGINT NOT NULL COMMENT '用户id', `order_id` BIGINT NOT NULL COMMENT '订单id', `remark` VARCHAR(200) DEFAULT '' COMMENT '备注', primary key (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='Spring Boot Demo 分库分表 系列示例表0'; DROP TABLE IF EXISTS `t_order_1`; CREATE TABLE `t_order_1` ( `id` BIGINT NOT NULL COMMENT '主键', `user_id` BIGINT NOT NULL COMMENT '用户id', `order_id` BIGINT NOT NULL COMMENT '订单id', `remark` VARCHAR(200) DEFAULT '' COMMENT '备注', primary key (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='Spring Boot Demo 分库分表 系列示例表1'; DROP TABLE IF EXISTS `t_order_2`; CREATE TABLE `t_order_2` ( `id` BIGINT NOT NULL COMMENT '主键', `user_id` BIGINT NOT NULL COMMENT '用户id', `order_id` BIGINT NOT NULL COMMENT '订单id', `remark` VARCHAR(200) DEFAULT '' COMMENT '备注', primary key (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='Spring Boot Demo 分库分表 系列示例表2'; ================================================ FILE: demo-sharding-jdbc/src/main/java/com/xkcoding/sharding/jdbc/SpringBootDemoShardingJdbcApplication.java ================================================ package com.xkcoding.sharding.jdbc; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.transaction.annotation.EnableTransactionManagement; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-01-23 22:05 */ @SpringBootApplication @EnableTransactionManagement(proxyTargetClass = true) @MapperScan("com.xkcoding.sharding.jdbc.mapper") public class SpringBootDemoShardingJdbcApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoShardingJdbcApplication.class, args); } } ================================================ FILE: demo-sharding-jdbc/src/main/java/com/xkcoding/sharding/jdbc/config/CustomSnowflakeKeyGenerator.java ================================================ package com.xkcoding.sharding.jdbc.config; import cn.hutool.core.lang.Snowflake; import io.shardingsphere.core.keygen.KeyGenerator; /** *

    * 自定义雪花算法,替换 DefaultKeyGenerator,避免DefaultKeyGenerator生成的id大几率是偶数 *

    * * @author yangkai.shen * @date Created in 2019-03-26 17:07 */ public class CustomSnowflakeKeyGenerator implements KeyGenerator { private Snowflake snowflake; public CustomSnowflakeKeyGenerator(Snowflake snowflake) { this.snowflake = snowflake; } @Override public Number generateKey() { return snowflake.nextId(); } } ================================================ FILE: demo-sharding-jdbc/src/main/java/com/xkcoding/sharding/jdbc/config/DataSourceShardingConfig.java ================================================ package com.xkcoding.sharding.jdbc.config; import cn.hutool.core.lang.Snowflake; import cn.hutool.core.util.IdUtil; import com.zaxxer.hikari.HikariDataSource; import io.shardingsphere.api.config.rule.ShardingRuleConfiguration; import io.shardingsphere.api.config.rule.TableRuleConfiguration; import io.shardingsphere.api.config.strategy.InlineShardingStrategyConfiguration; import io.shardingsphere.api.config.strategy.NoneShardingStrategyConfiguration; import io.shardingsphere.core.keygen.KeyGenerator; import io.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; /** *

    * sharding-jdbc 的数据源配置 *

    * * @author yangkai.shen * @date Created in 2019-03-26 16:47 */ @Configuration public class DataSourceShardingConfig { private static final Snowflake snowflake = IdUtil.createSnowflake(1, 1); /** * 需要手动配置事务管理器 */ @Bean public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "dataSource") @Primary public DataSource dataSource() throws SQLException { ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); // 设置分库策略 shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}")); // 设置规则适配的表 shardingRuleConfig.getBindingTableGroups().add("t_order"); // 设置分表策略 shardingRuleConfig.getTableRuleConfigs().add(orderTableRule()); shardingRuleConfig.setDefaultDataSourceName("ds0"); shardingRuleConfig.setDefaultTableShardingStrategyConfig(new NoneShardingStrategyConfiguration()); Properties properties = new Properties(); properties.setProperty("sql.show", "true"); return ShardingDataSourceFactory.createDataSource(dataSourceMap(), shardingRuleConfig, new ConcurrentHashMap<>(16), properties); } private TableRuleConfiguration orderTableRule() { TableRuleConfiguration tableRule = new TableRuleConfiguration(); // 设置逻辑表名 tableRule.setLogicTable("t_order"); // ds${0..1}.t_order_${0..2} 也可以写成 ds$->{0..1}.t_order_$->{0..1} tableRule.setActualDataNodes("ds${0..1}.t_order_${0..2}"); tableRule.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order_$->{order_id % 3}")); tableRule.setKeyGenerator(customKeyGenerator()); tableRule.setKeyGeneratorColumnName("order_id"); return tableRule; } private Map dataSourceMap() { Map dataSourceMap = new HashMap<>(16); // 配置第一个数据源 HikariDataSource ds0 = new HikariDataSource(); ds0.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds0.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8"); ds0.setUsername("root"); ds0.setPassword("root"); // 配置第二个数据源 HikariDataSource ds1 = new HikariDataSource(); ds1.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds1.setJdbcUrl("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"); ds1.setUsername("root"); ds1.setPassword("root"); dataSourceMap.put("ds0", ds0); dataSourceMap.put("ds1", ds1); return dataSourceMap; } /** * 自定义主键生成器 */ private KeyGenerator customKeyGenerator() { return new CustomSnowflakeKeyGenerator(snowflake); } } ================================================ FILE: demo-sharding-jdbc/src/main/java/com/xkcoding/sharding/jdbc/mapper/OrderMapper.java ================================================ package com.xkcoding.sharding.jdbc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.xkcoding.sharding.jdbc.model.Order; import org.springframework.stereotype.Component; /** *

    * 订单表 Mapper *

    * * @author yangkai.shen * @date Created in 2019-03-26 13:38 */ @Component public interface OrderMapper extends BaseMapper { } ================================================ FILE: demo-sharding-jdbc/src/main/java/com/xkcoding/sharding/jdbc/model/Order.java ================================================ package com.xkcoding.sharding.jdbc.model; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** *

    * 订单表 *

    * * @author yangkai.shen * @date Created in 2019-03-26 13:35 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @TableName(value = "t_order") public class Order { /** * 主键 */ private Long id; /** * 用户id */ private Long userId; /** * 订单id */ private Long orderId; /** * 备注 */ private String remark; } ================================================ FILE: demo-sharding-jdbc/src/main/resources/application.yml ================================================ mybatis-plus: global-config: banner: false ================================================ FILE: demo-sharding-jdbc/src/test/java/com/xkcoding/sharding/jdbc/SpringBootDemoShardingJdbcApplicationTests.java ================================================ package com.xkcoding.sharding.jdbc; import cn.hutool.core.util.RandomUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.xkcoding.sharding.jdbc.mapper.OrderMapper; import com.xkcoding.sharding.jdbc.model.Order; 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; /** *

    * 测试sharding-jdbc分库分表 *

    * * @author yangkai.shen * @date Created in 2019-03-26 13:44 */ @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class SpringBootDemoShardingJdbcApplicationTests { @Autowired private OrderMapper orderMapper; /** * 测试新增 */ @Test public void testInsert() { for (long i = 1; i < 10; i++) { for (long j = 1; j < 20; j++) { Order order = Order.builder().userId(i).orderId(j).remark(RandomUtil.randomString(20)).build(); orderMapper.insert(order); } } } /** * 测试更新 */ @Test public void testUpdate() { Order update = new Order(); update.setRemark("修改备注信息"); orderMapper.update(update, Wrappers.update().lambda().eq(Order::getOrderId, 2).eq(Order::getUserId, 2)); } /** * 测试删除 */ @Test public void testDelete() { orderMapper.delete(new QueryWrapper<>()); } /** * 测试查询 */ @Test public void testSelect() { List orders = orderMapper.selectList(Wrappers.query().lambda().in(Order::getOrderId, 1, 2)); log.info("【orders】= {}", JSONUtil.toJsonStr(orders)); } } ================================================ FILE: demo-social/.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-social/README.md ================================================ # spring-boot-demo-social > 此 demo 主要演示 Spring Boot 项目如何使用 **[史上最全的第三方登录工具 - JustAuth](https://github.com/zhangyd-c/JustAuth)** 实现第三方登录,包括QQ登录、GitHub登录、微信登录、谷歌登录、微软登录、小米登录、企业微信登录。 > > 通过 [justauth-spring-boot-starter](https://search.maven.org/artifact/com.xkcoding/justauth-spring-boot-starter) 快速集成,好嗨哟~ > > JustAuth,如你所见,它仅仅是一个**第三方授权登录**的**工具类库**,它可以让我们脱离繁琐的第三方登录SDK,让登录变得**So easy!** > > 1. **全**:已集成十多家第三方平台(国内外常用的基本都已包含),后续依然还有扩展计划! >2. **简**:API就是奔着最简单去设计的(见后面[`快速开始`](https://github.com/zhangyd-c/JustAuth#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)),尽量让您用起来没有障碍感! > >PS: 本人十分幸运的参与到了这个SDK的开发,主要开发了**QQ登录、微信登录、小米登录、微软登录、谷歌登录**这 **`5`** 个第三方登录,以及一些BUG的修复工作。再次感谢 [@母狼](https://github.com/zhangyd-c) 开源这个又好用又全面的第三方登录SDK。 如果技术选型是 `JFinal` 的,请查看此 [**`demo`**](https://github.com/xkcoding/jfinal-justauth-demo) https://github.com/xkcoding/jfinal-justauth-demo 如果技术选型是 `ActFramework` 的,请查看此 [**`demo`**](https://github.com/xkcoding/act-justauth-demo) https://github.com/xkcoding/act-justauth-demo ## 1. 环境准备 ### 1.1. 公网服务器准备 首先准备一台有公网IP的服务器,可以选用阿里云或者腾讯云,如果选用的是阿里云的,可以使用我的[优惠链接](https://chuangke.aliyun.com/invite?userCode=r8z5amhr)购买。 ### 1.2. 内网穿透frp搭建 > frp 安装程序:https://github.com/fatedier/frp/releases #### 1.2.1. frp服务端搭建 服务端搭建在上一步准备的公网服务器上,因为服务器是centos7 x64的系统,因此,这里下载安装包版本为linux_amd64的 [frp_0.27.0_linux_amd64.tar.gz](https://github.com/fatedier/frp/releases/download/v0.27.0/frp_0.27.0_linux_amd64.tar.gz) 。 1. 下载安装包 ```shell $ wget https://github.com/fatedier/frp/releases/download/v0.27.0/frp_0.27.0_linux_amd64.tar.gz ``` 2. 解压安装包 ```shell $ tar -zxvf frp_0.27.0_linux_amd64.tar.gz ``` 3. 修改配置文件 ```shell $ cd frp_0.27.0_linux_amd64 $ vim frps.ini [common] bind_port = 7100 vhost_http_port = 7200 ``` 4. 启动frp服务端 ```shell $ ./frps -c frps.ini 2019/06/15 16:42:02 [I] [service.go:139] frps tcp listen on 0.0.0.0:7100 2019/06/15 16:42:02 [I] [service.go:181] http service listen on 0.0.0.0:7200 2019/06/15 16:42:02 [I] [root.go:204] Start frps success ``` #### 1.2.2. frp客户端搭建 客户端搭建在本地的Mac上,因此下载安装包版本为darwin_amd64的 [frp_0.27.0_darwin_amd64.tar.gz](https://github.com/fatedier/frp/releases/download/v0.27.0/frp_0.27.0_darwin_amd64.tar.gz) 。 1. 下载安装包 ```shell $ wget https://github.com/fatedier/frp/releases/download/v0.27.0/frp_0.27.0_darwin_amd64.tar.gz ``` 2. 解压安装包 ```shell $ tar -zxvf frp_0.27.0_darwin_amd64.tar.gz ``` 3. 修改配置文件,配置服务端ip端口及监听的域名信息 ```shell $ cd frp_0.27.0_darwin_amd64 $ vim frpc.ini [common] server_addr = 120.92.169.103 server_port = 7100 [web] type = http local_port = 8080 custom_domains = oauth.xkcoding.com ``` 4. 启动frp客户端 ```shell $ ./frpc -c frpc.ini 2019/06/15 16:48:52 [I] [service.go:221] login to server success, get run id [8bb83bae5c58afe6], server udp port [0] 2019/06/15 16:48:52 [I] [proxy_manager.go:137] [8bb83bae5c58afe6] proxy added: [web] 2019/06/15 16:48:52 [I] [control.go:144] [web] start proxy success ``` ### 1.3. 配置域名解析 前往阿里云DNS解析,将域名解析到我们的公网服务器上,比如我的就是将 `oauth.xkcoding.com -> 120.92.169.103` ![image-20190615165843639](http://static.xkcoding.com/spring-boot-demo/social/063923.jpg) ### 1.4. nginx代理 nginx 的搭建就不在此赘述了,只说配置 ```nginx server { listen 80; server_name oauth.xkcoding.com; location / { proxy_pass http://127.0.0.1:7200; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_buffering off; sendfile off; proxy_max_temp_file_size 0; client_max_body_size 10m; client_body_buffer_size 128k; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; proxy_temp_file_write_size 64k; proxy_http_version 1.1; proxy_request_buffering off; } } ``` 测试配置文件是否有问题 ```shell $ nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` 重新加载配置文件,使其生效 ```shell $ nginx -s reload ``` > 现在当我们在浏览器输入 `oauth.xkcoding.com` 的时候,网络流量其实会经历以下几个步骤: > > 1. 通过之前配的DNS域名解析会访问到我们的公网服务器 `120.92.169.103` 的 80 端口 > 2. 再经过 nginx,代理到本地的 7200 端口 > 3. 再经过 frp 穿透到我们的 Mac 电脑的 8080 端口 > 4. 此时 8080 就是我们的应用程序端口 ### 1.5. 第三方平台申请 #### 1.5.1. QQ互联平台申请 1. 前往 https://connect.qq.com/ 2. 申请开发者 3. 应用管理 -> 添加网站应用,等待审核通过即可 ![image-20190617144655429](http://static.xkcoding.com/spring-boot-demo/social/063921-1.jpg) #### 1.5.2. GitHub平台申请 1. 前往 https://github.com/settings/developers 2. 点击 `New OAuth App` 按钮创建应用 ![image-20190617145839851](http://static.xkcoding.com/spring-boot-demo/social/063919.jpg) #### 1.5.3 微信开放平台申请 这里微信开放平台需要用企业的,个人没有资质,所以我在某宝租了一个月的资质,需要的可以 [戳我租赁](https://item.taobao.com/item.htm?spm=2013.1.w4023-5034755838.13.747a61a7ccfHwS&id=554942413474) > 声明:本人与该店铺无利益相关,纯属个人觉得好用做分享 > > 该店铺有两种方式: > > 1. 店铺支持帮你过企业资质,这里就用你自己的开放平台号就好了 > 2. 临时使用可以问店家租一个月进行开发,这里租了之后,店家会把 AppID 和 AppSecret 的信息发给你,你提供回调域就好了 因此这里我就贴出一张授权回调的地址作参考。 ![image-20190617153552218](http://static.xkcoding.com/spring-boot-demo/social/063927-1.jpg) #### 1.5.4. 谷歌开放平台申请 1. 前往 https://console.developers.google.com/projectcreate 创建项目 2. 前往 https://console.developers.google.com/apis/credentials ,在第一步创建的项目下,添加应用 ![image-20190617151119584](http://static.xkcoding.com/spring-boot-demo/social/063920.jpg) ![image-20190617150903039](http://static.xkcoding.com/spring-boot-demo/social/063922.jpg) #### 1.5.5. 微软开放平台申请 1. 前往 https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade 注册应用 2. 在注册应用的时候就需要填写回调地址,当然后期也可以重新修改 ![image-20190617152529449](http://static.xkcoding.com/spring-boot-demo/social/063921.jpg) 3. client id 在这里 ![image-20190617152805581](http://static.xkcoding.com/spring-boot-demo/social/063927.jpg) 4. client secret 需要自己在这里生成 ![image-20190617152711938](http://static.xkcoding.com/spring-boot-demo/social/063924.jpg) #### 1.5.6. 小米开放平台申请 1. 申请小米开发者,审核通过 2. 前往 https://dev.mi.com/passport/oauth2/applist 添加oauth应用,选择 `创建网页应用` 3. 填写基本信息之后,进入应用信息页面填写 `回调地址` ![image-20190617151502414](http://static.xkcoding.com/spring-boot-demo/social/063924-1.jpg) 4. 应用审核通过之后,可以在应用信息页面的 `应用详情` 查看到 AppKey 和 AppSecret,吐槽下,小米应用的审核速度特别慢,需要耐心等待。。。。 ![image-20190617151624603](http://static.xkcoding.com/spring-boot-demo/social/063926.jpg) #### 1.5.7. 企业微信平台申请 > 参考:https://xkcoding.com/2019/08/06/use-justauth-integration-wechat-enterprise.html ## 2. 主要代码 > 本 demo 采用 Redis 缓存 state,所以请准备 Redis 环境,如果没有 Redis 环境,可以将配置文件的缓存配置为 > > ```yaml > justauth: > cache: > type: default > ``` ### 2.1. pom.xml ```xml 4.0.0 spring-boot-demo-social 1.0.0-SNAPSHOT jar spring-boot-demo-social Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.1.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 com.xkcoding justauth-spring-boot-starter ${justauth-spring-boot.version} org.projectlombok lombok true com.google.guava guava cn.hutool hutool-all spring-boot-demo-social org.springframework.boot spring-boot-maven-plugin ``` ### 2.2. application.yml ```yaml server: port: 8080 servlet: context-path: /demo 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 justauth: enabled: true type: qq: client-id: 10******85 client-secret: 1f7d************************d629e redirect-uri: http://oauth.xkcoding.com/demo/oauth/qq/callback github: client-id: 2d25******d5f01086 client-secret: 5a2919b************************d7871306d1 redirect-uri: http://oauth.xkcoding.com/demo/oauth/github/callback wechat: client-id: wxdcb******4ff4 client-secret: b4e9dc************************a08ed6d redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat/callback google: client-id: 716******17-6db******vh******ttj320i******userco******t.com client-secret: 9IBorn************7-E redirect-uri: http://oauth.xkcoding.com/demo/oauth/google/callback microsoft: client-id: 7bdce8******************e194ad76c1b client-secret: Iu0zZ4************************tl9PWan_. redirect-uri: https://oauth.xkcoding.com/demo/oauth/microsoft/callback mi: client-id: 288************2994 client-secret: nFeTt89************************== redirect-uri: http://oauth.xkcoding.com/demo/oauth/mi/callback wechat_enterprise: client-id: ww58******f3************fbc client-secret: 8G6PCr00j************************rgk************AyzaPc78 redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_enterprise/callback agent-id: 1*******2 cache: type: redis prefix: 'SOCIAL::STATE::' timeout: 1h ``` ### 2.3. OauthController.java ```java /** *

    * 第三方登录 Controller *

    * * @author yangkai.shen * @date Created in 2019-05-17 10:07 */ @Slf4j @RestController @RequestMapping("/oauth") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class OauthController { private final AuthRequestFactory factory; /** * 登录类型 */ @GetMapping public Map loginType() { List oauthList = factory.oauthList(); return oauthList.stream().collect(Collectors.toMap(oauth -> oauth.toLowerCase() + "登录", oauth -> "http://oauth.xkcoding.com/demo/oauth/login/" + oauth.toLowerCase())); } /** * 登录 * * @param oauthType 第三方登录类型 * @param response response * @throws IOException */ @RequestMapping("/login/{oauthType}") public void renderAuth(@PathVariable String oauthType, HttpServletResponse response) throws IOException { AuthRequest authRequest = factory.get(getAuthSource(oauthType)); response.sendRedirect(authRequest.authorize(oauthType + "::" + AuthStateUtils.createState())); } /** * 登录成功后的回调 * * @param oauthType 第三方登录类型 * @param callback 携带返回的信息 * @return 登录成功后的信息 */ @RequestMapping("/{oauthType}/callback") public AuthResponse login(@PathVariable String oauthType, AuthCallback callback) { AuthRequest authRequest = factory.get(getAuthSource(oauthType)); AuthResponse response = authRequest.login(callback); log.info("【response】= {}", JSONUtil.toJsonStr(response)); return response; } private AuthSource getAuthSource(String type) { if (StrUtil.isNotBlank(type)) { return AuthSource.valueOf(type.toUpperCase()); } else { throw new RuntimeException("不支持的类型"); } } } ``` ### 2.4. 如果想要自定义 state 缓存 请看👉[这里](https://github.com/justauth/justauth-spring-boot-starter#2-%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE) ## 3. 运行方式 打开浏览器,输入 http://oauth.xkcoding.com/demo/oauth ,点击各个登录方式自行测试。 > `Google 登录,有可能因为祖国的强大导致测试失败,自行解决~` :kissing_smiling_eyes: ![image-20190809161032422](https://static.xkcoding.com/blog/2019-08-09-081033.png) ## 参考 1. JustAuth 项目地址:https://github.com/justauth/JustAuth 2. justauth-spring-boot-starter 地址:https://github.com/justauth/justauth-spring-boot-starter 3. frp内网穿透项目地址:https://github.com/fatedier/frp 4. frp内网穿透官方中文文档:https://github.com/fatedier/frp/blob/master/README_zh.md 5. Frp实现内网穿透:https://zhuanlan.zhihu.com/p/45445979 6. QQ互联文档:http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0 7. 微信开放平台文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN 8. GitHub第三方登录文档:https://developer.github.com/apps/building-oauth-apps/ 9. 谷歌Oauth2文档:https://developers.google.com/identity/protocols/OpenIDConnect 10. 微软Oauth2文档:https://docs.microsoft.com/zh-cn/graph/auth-v2-user 11. 小米开放平台账号服务文档:https://dev.mi.com/console/doc/detail?pId=707 ================================================ FILE: demo-social/pom.xml ================================================ 4.0.0 demo-social 1.0.0-SNAPSHOT jar demo-social Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.1.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 com.xkcoding justauth-spring-boot-starter ${justauth-spring-boot.version} org.projectlombok lombok true com.google.guava guava cn.hutool hutool-all demo-social org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-social/src/main/java/com/xkcoding/social/SpringBootDemoSocialApplication.java ================================================ package com.xkcoding.social; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-08-09 13:51 */ @SpringBootApplication public class SpringBootDemoSocialApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoSocialApplication.class, args); } } ================================================ FILE: demo-social/src/main/java/com/xkcoding/social/controller/OauthController.java ================================================ package com.xkcoding.social.controller; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.xkcoding.justauth.AuthRequestFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import me.zhyd.oauth.config.AuthSource; import me.zhyd.oauth.model.AuthCallback; import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.utils.AuthStateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** *

    * 第三方登录 Controller *

    * * @author yangkai.shen * @date Created in 2019-05-17 10:07 */ @Slf4j @RestController @RequestMapping("/oauth") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class OauthController { private final AuthRequestFactory factory; /** * 登录类型 */ @GetMapping public Map loginType() { List oauthList = factory.oauthList(); return oauthList.stream().collect(Collectors.toMap(oauth -> oauth.toLowerCase() + "登录", oauth -> "http://oauth.xkcoding.com/demo/oauth/login/" + oauth.toLowerCase())); } /** * 登录 * * @param oauthType 第三方登录类型 * @param response response * @throws IOException */ @RequestMapping("/login/{oauthType}") public void renderAuth(@PathVariable String oauthType, HttpServletResponse response) throws IOException { AuthRequest authRequest = factory.get(getAuthSource(oauthType)); response.sendRedirect(authRequest.authorize(oauthType + "::" + AuthStateUtils.createState())); } /** * 登录成功后的回调 * * @param oauthType 第三方登录类型 * @param callback 携带返回的信息 * @return 登录成功后的信息 */ @RequestMapping("/{oauthType}/callback") public AuthResponse login(@PathVariable String oauthType, AuthCallback callback) { AuthRequest authRequest = factory.get(getAuthSource(oauthType)); AuthResponse response = authRequest.login(callback); log.info("【response】= {}", JSONUtil.toJsonStr(response)); return response; } private AuthSource getAuthSource(String type) { if (StrUtil.isNotBlank(type)) { return AuthSource.valueOf(type.toUpperCase()); } else { throw new RuntimeException("不支持的类型"); } } } ================================================ FILE: demo-social/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo 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 justauth: enabled: true type: qq: client-id: 10******85 client-secret: 1f7d************************d629e redirect-uri: http://oauth.xkcoding.com/demo/oauth/qq/callback github: client-id: 2d25******d5f01086 client-secret: 5a2919b************************d7871306d1 redirect-uri: http://oauth.xkcoding.com/demo/oauth/github/callback wechat: client-id: wxdcb******4ff4 client-secret: b4e9dc************************a08ed6d redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat/callback google: client-id: 716******17-6db******vh******ttj320i******userco******t.com client-secret: 9IBorn************7-E redirect-uri: http://oauth.xkcoding.com/demo/oauth/google/callback microsoft: client-id: 7bdce8******************e194ad76c1b client-secret: Iu0zZ4************************tl9PWan_. redirect-uri: https://oauth.xkcoding.com/demo/oauth/microsoft/callback mi: client-id: 288************2994 client-secret: nFeTt89************************== redirect-uri: http://oauth.xkcoding.com/demo/oauth/mi/callback wechat_enterprise: client-id: ww58******f3************fbc client-secret: 8G6PCr00j************************rgk************AyzaPc78 redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_enterprise/callback agent-id: 1******2 cache: type: redis prefix: 'SOCIAL::STATE::' timeout: 1h ================================================ FILE: demo-social/src/test/java/com/xkcoding/social/SpringBootDemoSocialApplicationTests.java ================================================ package com.xkcoding.social; 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 SpringBootDemoSocialApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-swagger/.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-swagger/README.md ================================================ # spring-boot-demo-swagger > 此 demo 主要演示了 Spring Boot 如何集成原生 swagger ,自动生成 API 文档。 > > 启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/ # pom.xml ```xml 4.0.0 spring-boot-demo-swagger 1.0.0-SNAPSHOT jar spring-boot-demo-swagger Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.9.2 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test io.springfox springfox-swagger2 ${swagger.version} io.springfox springfox-swagger-ui ${swagger.version} org.projectlombok lombok true spring-boot-demo-swagger org.springframework.boot spring-boot-maven-plugin ``` ## Swagger2Config.java ```java /** *

    * Swagger2 配置 *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:14 */ @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.xkcoding.swagger.controller")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder().title("spring-boot-demo") .description("这是一个简单的 Swagger API 演示") .contact(new Contact("Yangkai.Shen", "http://xkcoding.com", "237497819@qq.com")) .version("1.0.0-SNAPSHOT") .build(); } } ``` ## UserController.java > 主要演示API层的注解。 ```java /** *

    * User Controller *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:30 */ @RestController @RequestMapping("/user") @Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理") @Slf4j public class UserController { @GetMapping @ApiOperation(value = "条件查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")}) public ApiResponse getByUserName(String username) { log.info("多个参数用 @ApiImplicitParams"); return ApiResponse.builder().code(200) .message("操作成功") .data(new User(1, username, "JAVA")) .build(); } @GetMapping("/{id}") @ApiOperation(value = "主键查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)}) public ApiResponse get(@PathVariable Integer id) { log.info("单个参数用 @ApiImplicitParam"); return ApiResponse.builder().code(200) .message("操作成功") .data(new User(id, "u1", "p1")) .build(); } @DeleteMapping("/{id}") @ApiOperation(value = "删除用户(DONE)", notes = "备注") @ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH) public void delete(@PathVariable Integer id) { log.info("单个参数用 ApiImplicitParam"); } @PostMapping @ApiOperation(value = "添加用户(DONE)") public User post(@RequestBody User user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/multipar") @ApiOperation(value = "添加用户(DONE)") public List multipar(@RequestBody List user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/array") @ApiOperation(value = "添加用户(DONE)") public User[] array(@RequestBody User[] user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PutMapping("/{id}") @ApiOperation(value = "修改用户(DONE)") public void put(@PathVariable Long id, @RequestBody User user) { log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 "); } @PostMapping("/{id}/file") @ApiOperation(value = "文件上传(DONE)") public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) { log.info(file.getContentType()); log.info(file.getName()); log.info(file.getOriginalFilename()); return file.getOriginalFilename(); } } ``` ## ApiResponse.java > 主要演示了 实体类 的注解。 ```java /** *

    * 通用API接口返回 *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:30 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "通用PI接口返回", description = "Common Api Response") public class ApiResponse implements Serializable { private static final long serialVersionUID = -8987146499044811408L; /** * 通用返回状态 */ @ApiModelProperty(value = "通用返回状态", required = true) private Integer code; /** * 通用返回信息 */ @ApiModelProperty(value = "通用返回信息", required = true) private String message; /** * 通用返回数据 */ @ApiModelProperty(value = "通用返回数据", required = true) private T data; } ``` ## 参考 1. swagger 官方网站:https://swagger.io/ 2. swagger 官方文档:https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Getting-started 3. swagger 常用注解:https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations ================================================ FILE: demo-swagger/pom.xml ================================================ 4.0.0 demo-swagger 1.0.0-SNAPSHOT jar demo-swagger Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.9.2 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test io.springfox springfox-swagger2 ${swagger.version} io.springfox springfox-swagger-ui ${swagger.version} org.projectlombok lombok true demo-swagger org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/SpringBootDemoSwaggerApplication.java ================================================ package com.xkcoding.swagger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-29 13:25 */ @SpringBootApplication public class SpringBootDemoSwaggerApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoSwaggerApplication.class, args); } } ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/common/ApiResponse.java ================================================ package com.xkcoding.swagger.common; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** *

    * 通用API接口返回 *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:30 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "通用PI接口返回", description = "Common Api Response") public class ApiResponse implements Serializable { private static final long serialVersionUID = -8987146499044811408L; /** * 通用返回状态 */ @ApiModelProperty(value = "通用返回状态", required = true) private Integer code; /** * 通用返回信息 */ @ApiModelProperty(value = "通用返回信息", required = true) private String message; /** * 通用返回数据 */ @ApiModelProperty(value = "通用返回数据", required = true) private T data; } ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/common/DataType.java ================================================ package com.xkcoding.swagger.common; /** *

    * 方便在 @ApiImplicitParam 的 dataType 属性使用 *

    * * @author yangkai.shen * @date Created in 2018-11-29 13:23 */ public final class DataType { public final static String STRING = "String"; public final static String INT = "int"; public final static String LONG = "long"; public final static String DOUBLE = "double"; public final static String FLOAT = "float"; public final static String BYTE = "byte"; public final static String BOOLEAN = "boolean"; public final static String ARRAY = "array"; public final static String BINARY = "binary"; public final static String DATETIME = "dateTime"; public final static String PASSWORD = "password"; } ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/common/ParamType.java ================================================ package com.xkcoding.swagger.common; /** *

    * 方便在 @ApiImplicitParam 的 paramType 属性使用 *

    * * @author yangkai.shen * @date Created in 2018-11-29 13:24 */ public final class ParamType { public final static String QUERY = "query"; public final static String HEADER = "header"; public final static String PATH = "path"; public final static String BODY = "body"; public final static String FORM = "form"; } ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/config/Swagger2Config.java ================================================ package com.xkcoding.swagger.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** *

    * Swagger2 配置 *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:14 */ @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.xkcoding.swagger.controller")).paths(PathSelectors.any()).build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder().title("spring-boot-demo").description("这是一个简单的 Swagger API 演示").contact(new Contact("Yangkai.Shen", "http://xkcoding.com", "237497819@qq.com")).version("1.0.0-SNAPSHOT").build(); } } ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/controller/UserController.java ================================================ package com.xkcoding.swagger.controller; import com.xkcoding.swagger.common.ApiResponse; import com.xkcoding.swagger.common.DataType; import com.xkcoding.swagger.common.ParamType; import com.xkcoding.swagger.entity.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; /** *

    * User Controller *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:30 */ @RestController @RequestMapping("/user") @Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理") @Slf4j public class UserController { @GetMapping @ApiOperation(value = "条件查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")}) public ApiResponse getByUserName(String username) { log.info("多个参数用 @ApiImplicitParams"); return ApiResponse.builder().code(200).message("操作成功").data(new User(1, username, "JAVA")).build(); } @GetMapping("/{id}") @ApiOperation(value = "主键查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)}) public ApiResponse get(@PathVariable Integer id) { log.info("单个参数用 @ApiImplicitParam"); return ApiResponse.builder().code(200).message("操作成功").data(new User(id, "u1", "p1")).build(); } @DeleteMapping("/{id}") @ApiOperation(value = "删除用户(DONE)", notes = "备注") @ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH) public void delete(@PathVariable Integer id) { log.info("单个参数用 ApiImplicitParam"); } @PostMapping @ApiOperation(value = "添加用户(DONE)") public User post(@RequestBody User user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/multipar") @ApiOperation(value = "添加用户(DONE)") public List multipar(@RequestBody List user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/array") @ApiOperation(value = "添加用户(DONE)") public User[] array(@RequestBody User[] user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PutMapping("/{id}") @ApiOperation(value = "修改用户(DONE)") public void put(@PathVariable Long id, @RequestBody User user) { log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 "); } @PostMapping("/{id}/file") @ApiOperation(value = "文件上传(DONE)") public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) { log.info(file.getContentType()); log.info(file.getName()); log.info(file.getOriginalFilename()); return file.getOriginalFilename(); } } ================================================ FILE: demo-swagger/src/main/java/com/xkcoding/swagger/entity/User.java ================================================ package com.xkcoding.swagger.entity; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** *

    * 用户实体 *

    * * @author yangkai.shen * @date Created in 2018-11-29 11:31 */ @Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "用户实体", description = "User Entity") public class User implements Serializable { private static final long serialVersionUID = 5057954049311281252L; /** * 主键id */ @ApiModelProperty(value = "主键id", required = true) private Integer id; /** * 用户名 */ @ApiModelProperty(value = "用户名", required = true) private String name; /** * 工作岗位 */ @ApiModelProperty(value = "工作岗位", required = true) private String job; } ================================================ FILE: demo-swagger/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ================================================ FILE: demo-swagger/src/test/java/com/xkcoding/swagger/SpringBootDemoSwaggerApplicationTests.java ================================================ package com.xkcoding.swagger; 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 SpringBootDemoSwaggerApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-swagger-beauty/.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-swagger-beauty/README.md ================================================ # spring-boot-demo-swagger-beauty > 此 demo 主要演示如何集成第三方的 swagger 来替换原生的 swagger,美化文档样式。本 demo 使用 [swagger-spring-boot-starter](https://github.com/battcn/swagger-spring-boot) 集成。 > > 启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/ > > 用户名:xkcoding > > 密码:123456 ## pom.xml ```xml 4.0.0 spring-boot-demo-swagger-beauty 1.0.0-SNAPSHOT jar spring-boot-demo-swagger-beauty Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.1.2-RELEASE org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.battcn swagger-spring-boot-starter ${battcn.swagger.version} org.projectlombok lombok true spring-boot-demo-swagger-beauty org.springframework.boot spring-boot-maven-plugin ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo spring: swagger: enabled: true title: spring-boot-demo description: 这是一个简单的 Swagger API 演示 version: 1.0.0-SNAPSHOT contact: name: Yangkai.Shen email: 237497819@qq.com url: http://xkcoding.com # swagger扫描的基础包,默认:全扫描 # base-package: # 需要处理的基础URL规则,默认:/** # base-path: # 需要排除的URL规则,默认:空 # exclude-path: security: # 是否启用 swagger 登录验证 filter-plugin: true username: xkcoding password: 123456 global-response-messages: GET[0]: code: 400 message: Bad Request,一般为请求参数不对 GET[1]: code: 404 message: NOT FOUND,一般为请求路径不对 GET[2]: code: 500 message: ERROR,一般为程序内部错误 POST[0]: code: 400 message: Bad Request,一般为请求参数不对 POST[1]: code: 404 message: NOT FOUND,一般为请求路径不对 POST[2]: code: 500 message: ERROR,一般为程序内部错误 ``` ## ApiResponse.java ```java /** *

    * 通用API接口返回 *

    * * @author yangkai.shen * @date Created in 2018-11-28 14:18 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "通用PI接口返回", description = "Common Api Response") public class ApiResponse implements Serializable { private static final long serialVersionUID = -8987146499044811408L; /** * 通用返回状态 */ @ApiModelProperty(value = "通用返回状态", required = true) private Integer code; /** * 通用返回信息 */ @ApiModelProperty(value = "通用返回信息", required = true) private String message; /** * 通用返回数据 */ @ApiModelProperty(value = "通用返回数据", required = true) private T data; } ``` ## User.java ```java /** *

    * 用户实体 *

    * * @author yangkai.shen * @date Created in 2018-11-28 14:13 */ @Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "用户实体", description = "User Entity") public class User implements Serializable { private static final long serialVersionUID = 5057954049311281252L; /** * 主键id */ @ApiModelProperty(value = "主键id", required = true) private Integer id; /** * 用户名 */ @ApiModelProperty(value = "用户名", required = true) private String name; /** * 工作岗位 */ @ApiModelProperty(value = "工作岗位", required = true) private String job; } ``` ## UserController.java ```java /** *

    * User Controller *

    * * @author yangkai.shen * @date Created in 2018-11-28 14:25 */ @RestController @RequestMapping("/user") @Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理") @Slf4j public class UserController { @GetMapping @ApiOperation(value = "条件查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")}) public ApiResponse getByUserName(String username) { log.info("多个参数用 @ApiImplicitParams"); return ApiResponse.builder().code(200).message("操作成功").data(new User(1, username, "JAVA")).build(); } @GetMapping("/{id}") @ApiOperation(value = "主键查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)}) public ApiResponse get(@PathVariable Integer id) { log.info("单个参数用 @ApiImplicitParam"); return ApiResponse.builder().code(200).message("操作成功").data(new User(id, "u1", "p1")).build(); } @DeleteMapping("/{id}") @ApiOperation(value = "删除用户(DONE)", notes = "备注") @ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH) public void delete(@PathVariable Integer id) { log.info("单个参数用 ApiImplicitParam"); } @PostMapping @ApiOperation(value = "添加用户(DONE)") public User post(@RequestBody User user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/multipar") @ApiOperation(value = "添加用户(DONE)") public List multipar(@RequestBody List user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/array") @ApiOperation(value = "添加用户(DONE)") public User[] array(@RequestBody User[] user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PutMapping("/{id}") @ApiOperation(value = "修改用户(DONE)") public void put(@PathVariable Long id, @RequestBody User user) { log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 "); } @PostMapping("/{id}/file") @ApiOperation(value = "文件上传(DONE)") public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) { log.info(file.getContentType()); log.info(file.getName()); log.info(file.getOriginalFilename()); return file.getOriginalFilename(); } } ``` ## 参考 - https://github.com/battcn/swagger-spring-boot/blob/master/README.md - 几款比较好看的swagger-ui,具体使用方法参见各个依赖的官方文档: - [battcn](https://github.com/battcn) 的 [swagger-spring-boot-starter](https://github.com/battcn/swagger-spring-boot) 文档:https://github.com/battcn/swagger-spring-boot/blob/master/README.md - [ swagger-ui-layer](https://gitee.com/caspar-chen/Swagger-UI-layer) 文档:https://gitee.com/caspar-chen/Swagger-UI-layer#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8 - [swagger-bootstrap-ui](https://gitee.com/xiaoym/swagger-bootstrap-ui) 文档:https://gitee.com/xiaoym/swagger-bootstrap-ui#%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E - [swagger-ui-themes](https://github.com/ostranme/swagger-ui-themes) 文档:https://github.com/ostranme/swagger-ui-themes#getting-started ================================================ FILE: demo-swagger-beauty/pom.xml ================================================ 4.0.0 demo-swagger-beauty 1.0.0-SNAPSHOT jar demo-swagger-beauty Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.1.2-RELEASE org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.battcn swagger-spring-boot-starter ${battcn.swagger.version} org.projectlombok lombok true demo-swagger-beauty org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-swagger-beauty/src/main/java/com/xkcoding/swagger/beauty/SpringBootDemoSwaggerBeautyApplication.java ================================================ package com.xkcoding.swagger.beauty; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-28 11:18 */ @SpringBootApplication public class SpringBootDemoSwaggerBeautyApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoSwaggerBeautyApplication.class, args); } } ================================================ FILE: demo-swagger-beauty/src/main/java/com/xkcoding/swagger/beauty/common/ApiResponse.java ================================================ package com.xkcoding.swagger.beauty.common; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** *

    * 通用API接口返回 *

    * * @author yangkai.shen * @date Created in 2018-11-28 14:18 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "通用PI接口返回", description = "Common Api Response") public class ApiResponse implements Serializable { private static final long serialVersionUID = -8987146499044811408L; /** * 通用返回状态 */ @ApiModelProperty(value = "通用返回状态", required = true) private Integer code; /** * 通用返回信息 */ @ApiModelProperty(value = "通用返回信息", required = true) private String message; /** * 通用返回数据 */ @ApiModelProperty(value = "通用返回数据", required = true) private T data; } ================================================ FILE: demo-swagger-beauty/src/main/java/com/xkcoding/swagger/beauty/controller/UserController.java ================================================ package com.xkcoding.swagger.beauty.controller; import com.battcn.boot.swagger.model.DataType; import com.battcn.boot.swagger.model.ParamType; import com.xkcoding.swagger.beauty.common.ApiResponse; import com.xkcoding.swagger.beauty.entity.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; /** *

    * User Controller *

    * * @author yangkai.shen * @date Created in 2018-11-28 14:25 */ @RestController @RequestMapping("/user") @Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理") @Slf4j public class UserController { @GetMapping @ApiOperation(value = "条件查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")}) public ApiResponse getByUserName(String username) { log.info("多个参数用 @ApiImplicitParams"); return ApiResponse.builder().code(200).message("操作成功").data(new User(1, username, "JAVA")).build(); } @GetMapping("/{id}") @ApiOperation(value = "主键查询(DONE)", notes = "备注") @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)}) public ApiResponse get(@PathVariable Integer id) { log.info("单个参数用 @ApiImplicitParam"); return ApiResponse.builder().code(200).message("操作成功").data(new User(id, "u1", "p1")).build(); } @DeleteMapping("/{id}") @ApiOperation(value = "删除用户(DONE)", notes = "备注") @ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH) public void delete(@PathVariable Integer id) { log.info("单个参数用 ApiImplicitParam"); } @PostMapping @ApiOperation(value = "添加用户(DONE)") public User post(@RequestBody User user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/multipar") @ApiOperation(value = "添加用户(DONE)") public List multipar(@RequestBody List user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PostMapping("/array") @ApiOperation(value = "添加用户(DONE)") public User[] array(@RequestBody User[] user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PutMapping("/{id}") @ApiOperation(value = "修改用户(DONE)") public void put(@PathVariable Long id, @RequestBody User user) { log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 "); } @PostMapping("/{id}/file") @ApiOperation(value = "文件上传(DONE)") public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) { log.info(file.getContentType()); log.info(file.getName()); log.info(file.getOriginalFilename()); return file.getOriginalFilename(); } } ================================================ FILE: demo-swagger-beauty/src/main/java/com/xkcoding/swagger/beauty/entity/User.java ================================================ package com.xkcoding.swagger.beauty.entity; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** *

    * 用户实体 *

    * * @author yangkai.shen * @date Created in 2018-11-28 14:13 */ @Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "用户实体", description = "User Entity") public class User implements Serializable { private static final long serialVersionUID = 5057954049311281252L; /** * 主键id */ @ApiModelProperty(value = "主键id", required = true) private Integer id; /** * 用户名 */ @ApiModelProperty(value = "用户名", required = true) private String name; /** * 工作岗位 */ @ApiModelProperty(value = "工作岗位", required = true) private String job; } ================================================ FILE: demo-swagger-beauty/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: swagger: enabled: true title: spring-boot-demo base-package: com.xkcoding.swagger.beauty.controller description: 这是一个简单的 Swagger API 演示 version: 1.0.0-SNAPSHOT contact: name: Yangkai.Shen email: 237497819@qq.com url: http://xkcoding.com # swagger扫描的基础包,默认:全扫描 # base-package: # 需要处理的基础URL规则,默认:/** # base-path: # 需要排除的URL规则,默认:空 # exclude-path: security: # 是否启用 swagger 登录验证 filter-plugin: true username: xkcoding password: 123456 global-response-messages: GET[0]: code: 400 message: Bad Request,一般为请求参数不对 GET[1]: code: 404 message: NOT FOUND,一般为请求路径不对 GET[2]: code: 500 message: ERROR,一般为程序内部错误 POST[0]: code: 400 message: Bad Request,一般为请求参数不对 POST[1]: code: 404 message: NOT FOUND,一般为请求路径不对 POST[2]: code: 500 message: ERROR,一般为程序内部错误 ================================================ FILE: demo-swagger-beauty/src/test/java/com/xkcoding/swagger/beauty/SpringBootDemoSwaggerBeautyApplicationTests.java ================================================ package com.xkcoding.swagger.beauty; 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 SpringBootDemoSwaggerBeautyApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-task/.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-task/README.md ================================================ # spring-boot-demo-task > 此 demo 主要演示了 Spring Boot 如何快速实现定时任务。 ## pom.xml ```xml 4.0.0 spring-boot-demo-task 1.0.0-SNAPSHOT jar spring-boot-demo-task 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.apache.commons commons-lang3 org.projectlombok lombok true cn.hutool hutool-all org.springframework.boot spring-boot-starter-test test spring-boot-demo-task org.springframework.boot spring-boot-maven-plugin ``` ## TaskConfig.java > 此处等同于在配置文件配置 > > ```properties > spring.task.scheduling.pool.size=20 > spring.task.scheduling.thread-name-prefix=Job-Thread- > ``` ```java /** *

    * 定时任务配置,配置线程池,使用不同线程执行任务,提升效率 *

    * * @author yangkai.shen * @date Created in 2018-11-22 19:02 */ @Configuration @EnableScheduling @ComponentScan(basePackages = {"com.xkcoding.task.job"}) public class TaskConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(taskExecutor()); } /** * 这里等同于配置文件配置 * {@code spring.task.scheduling.pool.size=20} - Maximum allowed number of threads. * {@code spring.task.scheduling.thread-name-prefix=Job-Thread- } - Prefix to use for the names of newly created threads. * {@link org.springframework.boot.autoconfigure.task.TaskSchedulingProperties} */ @Bean public Executor taskExecutor() { return new ScheduledThreadPoolExecutor(20, new BasicThreadFactory.Builder().namingPattern("Job-Thread-%d").build()); } } ``` ## TaskJob.java ```java /** *

    * 定时任务 *

    * * @author yangkai.shen * @date Created in 2018-11-22 19:09 */ @Component @Slf4j public class TaskJob { /** * 按照标准时间来算,每隔 10s 执行一次 */ @Scheduled(cron = "0/10 * * * * ?") public void job1() { log.info("【job1】开始执行:{}", DateUtil.formatDateTime(new Date())); } /** * 从启动时间开始,间隔 2s 执行 * 固定间隔时间 */ @Scheduled(fixedRate = 2000) public void job2() { log.info("【job2】开始执行:{}", DateUtil.formatDateTime(new Date())); } /** * 从启动时间开始,延迟 5s 后间隔 4s 执行 * 固定等待时间 */ @Scheduled(fixedDelay = 4000, initialDelay = 5000) public void job3() { log.info("【job3】开始执行:{}", DateUtil.formatDateTime(new Date())); } } ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo # 下面的配置等同于 TaskConfig #spring: # task: # scheduling: # pool: # size: 20 # thread-name-prefix: Job-Thread- ``` ## 参考 - Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-task-execution-scheduling ================================================ FILE: demo-task/pom.xml ================================================ 4.0.0 demo-task 1.0.0-SNAPSHOT jar demo-task 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.apache.commons commons-lang3 org.projectlombok lombok true cn.hutool hutool-all org.springframework.boot spring-boot-starter-test test demo-task org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-task/src/main/java/com/xkcoding/task/SpringBootDemoTaskApplication.java ================================================ package com.xkcoding.task; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-22 19:00 */ @SpringBootApplication public class SpringBootDemoTaskApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTaskApplication.class, args); } } ================================================ FILE: demo-task/src/main/java/com/xkcoding/task/config/TaskConfig.java ================================================ package com.xkcoding.task.config; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledThreadPoolExecutor; /** *

    * 定时任务配置,配置线程池,使用不同线程执行任务,提升效率 *

    * * @author yangkai.shen * @date Created in 2018-11-22 19:02 */ @Configuration @EnableScheduling @ComponentScan(basePackages = {"com.xkcoding.task.job"}) public class TaskConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(taskExecutor()); } /** * 这里等同于配置文件配置 * {@code spring.task.scheduling.pool.size=20} - Maximum allowed number of threads. * {@code spring.task.scheduling.thread-name-prefix=Job-Thread- } - Prefix to use for the names of newly created threads. * {@link org.springframework.boot.autoconfigure.task.TaskSchedulingProperties} */ @Bean public Executor taskExecutor() { return new ScheduledThreadPoolExecutor(20, new BasicThreadFactory.Builder().namingPattern("Job-Thread-%d").build()); } } ================================================ FILE: demo-task/src/main/java/com/xkcoding/task/job/TaskJob.java ================================================ package com.xkcoding.task.job; import cn.hutool.core.date.DateUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Date; /** *

    * 定时任务 *

    * * @author yangkai.shen * @date Created in 2018-11-22 19:09 */ @Component @Slf4j public class TaskJob { /** * 按照标准时间来算,每隔 10s 执行一次 */ @Scheduled(cron = "0/10 * * * * ?") public void job1() { log.info("【job1】开始执行:{}", DateUtil.formatDateTime(new Date())); } /** * 从启动时间开始,间隔 2s 执行 * 固定间隔时间 */ @Scheduled(fixedRate = 2000) public void job2() { log.info("【job2】开始执行:{}", DateUtil.formatDateTime(new Date())); } /** * 从启动时间开始,延迟 5s 后间隔 4s 执行 * 固定等待时间 */ @Scheduled(fixedDelay = 4000, initialDelay = 5000) public void job3() { log.info("【job3】开始执行:{}", DateUtil.formatDateTime(new Date())); } } ================================================ FILE: demo-task/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo # 下面的配置等同于 TaskConfig #spring: # task: # scheduling: # pool: # size: 20 # thread-name-prefix: Job-Thread- ================================================ FILE: demo-task/src/test/java/com/xkcoding/task/SpringBootDemoTaskApplicationTests.java ================================================ package com.xkcoding.task; 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 SpringBootDemoTaskApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-task-quartz/.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-task-quartz/README.md ================================================ # spring-boot-demo-task-quartz > 此 demo 主要演示了 Spring Boot 如何集成 Quartz 定时任务,并实现对定时任务的管理,包括新增定时任务,删除定时任务,暂停定时任务,恢复定时任务,修改定时任务启动时间,以及定时任务列表查询。 ## 后端 ### 初始化 在 `init/dbTables` 下选择 Quartz 需要的表结构,然后手动创建表。 ### pom.xml ```xml 4.0.0 spring-boot-demo-task-quartz 1.0.0-SNAPSHOT jar spring-boot-demo-task-quartz Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.1.0 1.2.10 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-quartz tk.mybatis mapper-spring-boot-starter ${mybatis.mapper.version} com.github.pagehelper pagehelper-spring-boot-starter ${mybatis.pagehelper.version} mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true spring-boot-demo-task-quartz org.springframework.boot spring-boot-maven-plugin ``` ### application.yml ```yaml server: port: 8080 servlet: context-path: /demo spring: # 省略其余配置,具体请 clone 本项目,查看详情 # ...... quartz: # 参见 org.springframework.boot.autoconfigure.quartz.QuartzProperties job-store-type: jdbc wait-for-jobs-to-complete-on-shutdown: true scheduler-name: SpringBootDemoScheduler properties: org.quartz.threadPool.threadCount: 5 org.quartz.threadPool.threadPriority: 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true org.quartz.jobStore.misfireThreshold: 5000 org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 在调度流程的第一步,也就是拉取待即将触发的triggers时,是上锁的状态,即不会同时存在多个线程拉取到相同的trigger的情况,也就避免的重复调度的危险。参考:https://segmentfault.com/a/1190000015492260 org.quartz.jobStore.acquireTriggersWithinLock: true # 省略其余配置,具体请 clone 本项目,查看详情 # ...... ``` --- > 后端其余代码请 clone 本项目,查看具体代码 ## 前端 > 前端页面请 clone 本项目,查看具体代码 ## 启动 1. clone 本项目 2. 初始化表格 3. 启动 `SpringBootDemoTaskQuartzApplication.java` 4. 打开浏览器,查看 http://localhost:8080/demo/job.html ![image-20181126214007372](http://static.xkcoding.com/spring-boot-demo/task/quartz/064006-1.jpg) ![image-20181126214109926](http://static.xkcoding.com/spring-boot-demo/task/quartz/064008.jpg) ![image-20181126214212905](http://static.xkcoding.com/spring-boot-demo/task/quartz/064009-1.jpg) ![image-20181126214138641](http://static.xkcoding.com/spring-boot-demo/task/quartz/064009.jpg) ![image-20181126214250757](http://static.xkcoding.com/spring-boot-demo/task/quartz/064007.jpg) ## 参考 - Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-quartz - Quartz 官方文档:http://www.quartz-scheduler.org/documentation/quartz-2.2.x/quick-start.html - Quartz 重复调度问题:https://segmentfault.com/a/1190000015492260 - 关于Quartz定时任务状态 (在 `QRTZ_TRIGGERS` 表中的 `TRIGGER_STATE` 字段) ![image-20181126171110378](http://static.xkcoding.com/spring-boot-demo/task/quartz/064006.jpg) - Vue.js 官方文档:https://cn.vuejs.org/v2/guide/ - Element-UI 官方文档:http://element-cn.eleme.io/#/zh-CN ================================================ FILE: demo-task-quartz/init/dbTables/tables_cloudscape.sql ================================================ # # Thanks to Srinivas Venkatarangaiah for submitting this file's contents # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.CloudscapeDelegate # # Known to work with Cloudscape 3.6.4 (should work with others) # create table qrtz_job_details ( sched_name varchar(120) not null, job_name varchar(200) not null, job_group varchar(200) not null, description varchar(250) , job_class_name varchar(250) not null, is_durable varchar(5) not null, is_nonconcurrent varchar(5) not null, is_update_data varchar(5) not null, requests_recovery varchar(5) not null, job_data long varbinary, primary key (sched_name,job_name,job_group) ); create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, job_name varchar(200) not null, job_group varchar(200) not null, description varchar(250) , next_fire_time longint, prev_fire_time longint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time longint not null, end_time longint, calendar_name varchar(200), misfire_instr smallint, job_data long varbinary, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, repeat_count longint not null, repeat_interval longint not null, times_triggered longint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); CREATE TABLE qrtz_simprop_triggers ( sched_name varchar(120) not null, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 longint NULL, LONG_PROP_2 longint NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 varchar(5) NULL, BOOL_PROP_2 varchar(5) NULL, PRIMARY KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(sched_name,TRIGGER_NAME,TRIGGER_GROUP) ); create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, blob_data long varbinary , primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(200) not null, calendar long varbinary not null, primary key (sched_name,calendar_name) ); create table qrtz_paused_trigger_grps ( sched_name varchar(120) not null, trigger_group varchar(200) not null, primary key (sched_name,trigger_group) ); create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, instance_name varchar(200) not null, fired_time longint not null, sched_time longint not null, priority integer not null, state varchar(16) not null, job_name varchar(200) null, job_group varchar(200) null, is_nonconcurrent varchar(5) null, requests_recovery varchar(5) null, primary key (sched_name,entry_id) ); create table qrtz_scheduler_state ( sched_name varchar(120) not null, instance_name varchar(200) not null, last_checkin_time longint not null, checkin_interval longint not null, primary key (sched_name,instance_name) ); create table qrtz_locks ( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_cubrid.sql ================================================ -- Thanks to Timothy Anyona for this script -- CUBRID 8.4.1+ -- In your Quartz properties file, you'll need to set -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.CUBRIDDelegate DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; DROP TABLE IF EXISTS QRTZ_LOCKS; DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; DROP TABLE IF EXISTS QRTZ_TRIGGERS; DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; DROP TABLE IF EXISTS QRTZ_CALENDARS; CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(250) NOT NULL, IS_DURABLE BIT(1) NOT NULL, IS_NONCONCURRENT BIT(1) NOT NULL, IS_UPDATE_DATA BIT(1) NOT NULL, REQUESTS_RECOVERY BIT(1) NOT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME BIGINT NULL, PREV_FIRE_TIME BIGINT NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT NOT NULL, END_TIME BIGINT NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR SMALLINT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT BIGINT NOT NULL, REPEAT_INTERVAL BIGINT NOT NULL, TIMES_TRIGGERED BIGINT NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(200) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 BIT(1) NULL, BOOL_PROP_2 BIT(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BLOB NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME BIGINT NOT NULL, SCHED_TIME BIGINT NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT BIT(1) NULL, REQUESTS_RECOVERY BIT(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME BIGINT NOT NULL, CHECKIN_INTERVAL BIGINT NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY); CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME); CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME); CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME); CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); ================================================ FILE: demo-task-quartz/init/dbTables/tables_db2.sql ================================================ # # Thanks to Horia Muntean for submitting this.... # # .. known to work with DB2 7.1 and the JDBC driver "COM.ibm.db2.jdbc.net.DB2Driver" # .. likely to work with others... # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # # If you're using DB2 6.x you'll want to set this property to # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.DB2v6Delegate # # Note that the blob column size (e.g. blob(2000)) dictates theount of data that can be stored in # that blob - i.e. limits theount of data you can put into your JobDataMap # create table qrtz_job_details ( sched_name varchar(120) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120) null, job_class_name varchar(128) not null, is_durable varchar(1) not null, is_nonconcurrent varchar(1) not null, is_update_data varchar(1) not null, requests_recovery varchar(1) not null, job_data blob(2000), primary key (sched_name,job_name,job_group) ) create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120) null, next_fire_time bigint, prev_fire_time bigint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time bigint not null, end_time bigint, calendar_name varchar(80), misfire_instr smallint, job_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ) create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, repeat_count bigint not null, repeat_interval bigint not null, times_triggered bigint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ) create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ) CREATE TABLE qrtz_simprop_triggers ( sched_name varchar(120) not null, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(sched_name,TRIGGER_NAME,TRIGGER_GROUP) ) create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, blob_data blob(2000) null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ) create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(80) not null, calendar blob(2000) not null, primary key (sched_name,calendar_name) ) create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, instance_name varchar(80) not null, fired_time bigint not null, sched_time bigint not null, priority integer not null, state varchar(16) not null, job_name varchar(80) null, job_group varchar(80) null, is_nonconcurrent varchar(1) null, requests_recovery varchar(1) null, primary key (sched_name,entry_id) ); create table qrtz_paused_trigger_grps( sched_name varchar(120) not null, trigger_group varchar(80) not null, primary key (sched_name,trigger_group) ); create table qrtz_scheduler_state ( sched_name varchar(120) not null, instance_name varchar(80) not null, last_checkin_time bigint not null, checkin_interval bigint not null, primary key (sched_name,instance_name) ); create table qrtz_locks ( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_db2_v72.sql ================================================ -- -- Thanks to Horia Muntean for submitting this, Mikkel Heisterberg for updating it -- -- .. known to work with DB2 7.2 and the JDBC driver "COM.ibm.db2.jdbc.net.DB2Driver" -- .. likely to work with others... -- -- In your Quartz properties file, you'll need to set -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.DB2v7Delegate -- -- or -- -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate -- -- If you're using DB2 6.x you'll want to set this property to -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.DB2v6Delegate -- -- Note that the blob column size (e.g. blob(2000)) dictates theount of data that can be stored in -- that blob - i.e. limits theount of data you can put into your JobDataMap -- DROP TABLE QRTZ_FIRED_TRIGGERS; DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE QRTZ_SCHEDULER_STATE; DROP TABLE QRTZ_LOCKS; DROP TABLE QRTZ_SIMPLE_TRIGGERS; DROP TABLE QRTZ_SIMPROP_TRIGGERS; DROP TABLE QRTZ_CRON_TRIGGERS; DROP TABLE QRTZ_TRIGGERS; DROP TABLE QRTZ_JOB_DETAILS; DROP TABLE QRTZ_CALENDARS; DROP TABLE QRTZ_BLOB_TRIGGERS; create table qrtz_job_details ( sched_name varchar(120) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120), job_class_name varchar(128) not null, is_durable varchar(1) not null, is_nonconcurrent varchar(1) not null, is_update_data varchar(1) not null, requests_recovery varchar(1) not null, job_data blob(2000), primary key (sched_name,job_name,job_group) ); create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120), next_fire_time bigint, prev_fire_time bigint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time bigint not null, end_time bigint, calendar_name varchar(80), misfire_instr smallint, job_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, repeat_count bigint not null, repeat_interval bigint not null, times_triggered bigint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); CREATE TABLE qrtz_simprop_triggers ( sched_name varchar(120) not null, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(sched_name,TRIGGER_NAME,TRIGGER_GROUP) ); create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, blob_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(80) not null, calendar blob(2000) not null, primary key (sched_name,calendar_name) ); create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, instance_name varchar(80) not null, fired_time bigint not null, sched_time bigint not null, priority integer not null, state varchar(16) not null, job_name varchar(80), job_group varchar(80), is_nonconcurrent varchar(1), requests_recovery varchar(1), primary key (sched_name,entry_id) ); create table qrtz_paused_trigger_grps( sched_name varchar(120) not null, trigger_group varchar(80) not null, primary key (sched_name,trigger_group) ); create table qrtz_scheduler_state ( sched_name varchar(120) not null, instance_name varchar(80) not null, last_checkin_time bigint not null, checkin_interval bigint not null, primary key (sched_name,instance_name) ); create table qrtz_locks ( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_db2_v8.sql ================================================ # # Updated by Claudiu Crisan (claudiu.crisan@schartner.net) # SQL scripts for DB2 ver 8.1 # # Changes: # - "varchar(1)" replaced with "integer" # - "field_name varchar(xxx) not null" replaced with "field_name varchar(xxx)" # DROP TABLE QRTZ_FIRED_TRIGGERS; DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE QRTZ_SCHEDULER_STATE; DROP TABLE QRTZ_LOCKS; DROP TABLE QRTZ_SIMPLE_TRIGGERS; DROP TABLE QRTZ_SIMPROP_TRIGGERS; DROP TABLE QRTZ_CRON_TRIGGERS; DROP TABLE QRTZ_TRIGGERS; DROP TABLE QRTZ_JOB_DETAILS; DROP TABLE QRTZ_CALENDARS; DROP TABLE QRTZ_BLOB_TRIGGERS; create table qrtz_job_details( sched_name varchar(120) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120), job_class_name varchar(128) not null, is_durable integer not null, is_nonconcurrent integer not null, is_update_data integer not null, requests_recovery integer not null, job_data blob(2000), primary key (sched_name,job_name,job_group) ); create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120), next_fire_time bigint, prev_fire_time bigint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time bigint not null, end_time bigint, calendar_name varchar(80), misfire_instr smallint, job_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, repeat_count bigint not null, repeat_interval bigint not null, times_triggered bigint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); CREATE TABLE qrtz_simprop_triggers ( sched_name varchar(120) not null, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(sched_name,TRIGGER_NAME,TRIGGER_GROUP) ); create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, blob_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(80) not null, calendar blob(2000) not null, primary key (calendar_name) ); create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, instance_name varchar(80) not null, fired_time bigint not null, sched_time bigint not null, priority integer not null, state varchar(16) not null, job_name varchar(80), job_group varchar(80), is_nonconcurrent integer, requests_recovery integer, primary key (sched_name,entry_id) ); create table qrtz_paused_trigger_grps( sched_name varchar(120) not null, trigger_group varchar(80) not null, primary key (sched_name,trigger_group) ); create table qrtz_scheduler_state( sched_name varchar(120) not null, instance_name varchar(80) not null, last_checkin_time bigint not null, checkin_interval bigint not null, primary key (sched_name,instance_name) ); create table qrtz_locks( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_db2_v95.sql ================================================ DROP TABLE QRTZ_FIRED_TRIGGERS; DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE QRTZ_SCHEDULER_STATE; DROP TABLE QRTZ_LOCKS; DROP TABLE QRTZ_SIMPLE_TRIGGERS; DROP TABLE QRTZ_SIMPROP_TRIGGERS; DROP TABLE QRTZ_CRON_TRIGGERS; DROP TABLE QRTZ_TRIGGERS; DROP TABLE QRTZ_JOB_DETAILS; DROP TABLE QRTZ_CALENDARS; DROP TABLE QRTZ_BLOB_TRIGGERS; create table qrtz_job_details( sched_name varchar(120) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120), job_class_name varchar(128) not null, is_durable integer not null, is_nonconcurrent integer not null, is_update_data integer not null, requests_recovery integer not null, job_data blob(2000), primary key (sched_name,job_name,job_group) ); create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120), next_fire_time bigint, prev_fire_time bigint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time bigint not null, end_time bigint, calendar_name varchar(80), misfire_instr smallint, job_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, repeat_count bigint not null, repeat_interval bigint not null, times_triggered bigint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); CREATE TABLE qrtz_simprop_triggers ( sched_name varchar(120) not null, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512), STR_PROP_2 VARCHAR(512), STR_PROP_3 VARCHAR(512), INT_PROP_1 INT, INT_PROP_2 INT, LONG_PROP_1 BIGINT, LONG_PROP_2 BIGINT, DEC_PROP_1 NUMERIC(13,4), DEC_PROP_2 NUMERIC(13,4), BOOL_PROP_1 VARCHAR(1), BOOL_PROP_2 VARCHAR(1), PRIMARY KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (sched_name,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(sched_name,TRIGGER_NAME,TRIGGER_GROUP) ); create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, blob_data blob(2000), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(80) not null, calendar blob(2000) not null, primary key (calendar_name) ); create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, instance_name varchar(80) not null, fired_time bigint not null, sched_time bigint not null, priority integer not null, state varchar(16) not null, job_name varchar(80), job_group varchar(80), is_nonconcurrent integer, requests_recovery integer, primary key (sched_name,entry_id) ); create table qrtz_paused_trigger_grps( sched_name varchar(120) not null, trigger_group varchar(80) not null, primary key (sched_name,trigger_group) ); create table qrtz_scheduler_state( sched_name varchar(120) not null, instance_name varchar(80) not null, last_checkin_time bigint not null, checkin_interval bigint not null, primary key (sched_name,instance_name) ); create table qrtz_locks( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_derby.sql ================================================ -- -- Apache Derby scripts by Steve Stewart, updated by Ronald Pomeroy -- Based on Srinivas Venkatarangaiah's file for Cloudscape -- -- Known to work with Apache Derby 10.0.2.1, or 10.6.2.1 -- -- Updated by Zemian Deng on 08/21/2011 -- * Fixed nullable fields on qrtz_simprop_triggers table. -- * Added Derby QuickStart comments and drop tables statements. -- -- DerbyDB + Quartz Quick Guide: -- * Derby comes with Oracle JDK! For Java6, it default install into C:/Program Files/Sun/JavaDB on Windows. -- 1. Create a derby.properties file under JavaDB directory, and have the following: -- derby.connection.requireAuthentication = true -- derby.authentication.provider = BUILTIN -- derby.user.quartz2=quartz2123 -- 2. Start the DB server by running bin/startNetworkServer script. -- 3. On a new terminal, run bin/ij tool to bring up an SQL prompt, then run: -- connect 'jdbc:derby://localhost:1527/quartz2;user=quartz2;password=quartz2123;create=true'; -- run 'quartz/docs/dbTables/tables_derby.sql'; -- Now in quartz.properties, you may use these properties: -- org.quartz.dataSource.quartzDataSource.driver = org.apache.derby.jdbc.ClientDriver -- org.quartz.dataSource.quartzDataSource.URL = jdbc:derby://localhost:1527/quartz2 -- org.quartz.dataSource.quartzDataSource.user = quartz2 -- org.quartz.dataSource.quartzDataSource.password = quartz2123 -- -- Auto drop and reset tables -- Derby doesn't support if exists condition on table drop, so user must manually do this step if needed to. -- drop table qrtz_fired_triggers; -- drop table qrtz_paused_trigger_grps; -- drop table qrtz_scheduler_state; -- drop table qrtz_locks; -- drop table qrtz_simple_triggers; -- drop table qrtz_simprop_triggers; -- drop table qrtz_cron_triggers; -- drop table qrtz_blob_triggers; -- drop table qrtz_triggers; -- drop table qrtz_job_details; -- drop table qrtz_calendars; create table qrtz_job_details ( sched_name varchar(120) not null, job_name varchar(200) not null, job_group varchar(200) not null, description varchar(250) , job_class_name varchar(250) not null, is_durable varchar(5) not null, is_nonconcurrent varchar(5) not null, is_update_data varchar(5) not null, requests_recovery varchar(5) not null, job_data blob, primary key (sched_name,job_name,job_group) ); create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, job_name varchar(200) not null, job_group varchar(200) not null, description varchar(250), next_fire_time bigint, prev_fire_time bigint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time bigint not null, end_time bigint, calendar_name varchar(200), misfire_instr smallint, job_data blob, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, repeat_count bigint not null, repeat_interval bigint not null, times_triggered bigint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_simprop_triggers ( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, str_prop_1 varchar(512), str_prop_2 varchar(512), str_prop_3 varchar(512), int_prop_1 int, int_prop_2 int, long_prop_1 bigint, long_prop_2 bigint, dec_prop_1 numeric(13,4), dec_prop_2 numeric(13,4), bool_prop_1 varchar(5), bool_prop_2 varchar(5), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, blob_data blob, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(200) not null, calendar blob not null, primary key (sched_name,calendar_name) ); create table qrtz_paused_trigger_grps ( sched_name varchar(120) not null, trigger_group varchar(200) not null, primary key (sched_name,trigger_group) ); create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, instance_name varchar(200) not null, fired_time bigint not null, sched_time bigint not null, priority integer not null, state varchar(16) not null, job_name varchar(200), job_group varchar(200), is_nonconcurrent varchar(5), requests_recovery varchar(5), primary key (sched_name,entry_id) ); create table qrtz_scheduler_state ( sched_name varchar(120) not null, instance_name varchar(200) not null, last_checkin_time bigint not null, checkin_interval bigint not null, primary key (sched_name,instance_name) ); create table qrtz_locks ( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_derby_previous.sql ================================================ -- -- Apache Derby scripts by Steve Stewart. -- Based on Srinivas Venkatarangaiah's file for Cloudscape -- -- In your Quartz properties file, you'll need to set -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.CloudscapeDelegate -- -- Known to work with Apache Derby 10.0.2.1 -- create table qrtz_job_details ( sched_name varchar(120) not null, job_name varchar(200) not null, job_group varchar(200) not null, description varchar(250) , job_class_name varchar(250) not null, is_durable varchar(5) not null, is_nonconcurrent varchar(5) not null, is_update_data varchar(5) not null, requests_recovery varchar(5) not null, job_data blob, primary key (sched_name,job_name,job_group) ); create table qrtz_triggers ( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, job_name varchar(200) not null, job_group varchar(200) not null, description varchar(250) , next_fire_time bigint, prev_fire_time bigint, priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time bigint not null, end_time bigint, calendar_name varchar(200), misfire_instr smallint, job_data blob, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers ( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, repeat_count bigint not null, repeat_interval bigint not null, times_triggered bigint not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers ( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_simprop_triggers ( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, str_prop_1 varchar(512), str_prop_2 varchar(512), str_prop_3 varchar(512), int_prop_1 int, int_prop_2 int, long_prop_1 bigint, long_prop_2 bigint, dec_prop_1 numeric(13,4), dec_prop_2 numeric(13,4), bool_prop_1 varchar(5), bool_prop_2 varchar(5), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_blob_triggers ( sched_name varchar(120) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, blob_data blob , primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars ( sched_name varchar(120) not null, calendar_name varchar(200) not null, calendar blob not null, primary key (sched_name,calendar_name) ); create table qrtz_paused_trigger_grps ( sched_name varchar(120) not null, trigger_group varchar(200) not null, primary key (sched_name,trigger_group) ); create table qrtz_fired_triggers ( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(200) not null, trigger_group varchar(200) not null, instance_name varchar(200) not null, fired_time bigint not null, sched_time bigint not null, priority integer not null, state varchar(16) not null, job_name varchar(200), job_group varchar(200), is_nonconcurrent varchar(5), requests_recovery varchar(5), primary key (sched_name,entry_id) ); create table qrtz_scheduler_state ( sched_name varchar(120) not null, instance_name varchar(200) not null, last_checkin_time bigint not null, checkin_interval bigint not null, primary key (sched_name,instance_name) ); create table qrtz_locks ( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_firebird.sql ================================================ -- -- Thanks to Leonardo Alves -- DROP TABLE QRTZ_FIRED_TRIGGERS; DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE QRTZ_SCHEDULER_STATE; DROP TABLE QRTZ_LOCKS; DROP TABLE QRTZ_SIMPLE_TRIGGERS; DROP TABLE QRTZ_SIMPROP_TRIGGERS; DROP TABLE QRTZ_CRON_TRIGGERS; DROP TABLE QRTZ_BLOB_TRIGGERS; DROP TABLE QRTZ_TRIGGERS; DROP TABLE QRTZ_JOB_DETAILS; DROP TABLE QRTZ_CALENDARS; CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(60) NOT NULL, JOB_GROUP VARCHAR(60) NOT NULL, DESCRIPTION VARCHAR(120), JOB_CLASS_NAME VARCHAR(128) NOT NULL, IS_DURABLE VARCHAR(1) NOT NULL, IS_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA BLOB, CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(60) NOT NULL, TRIGGER_GROUP VARCHAR(60) NOT NULL, JOB_NAME VARCHAR(60) NOT NULL, JOB_GROUP VARCHAR(60) NOT NULL, DESCRIPTION VARCHAR(120), NEXT_FIRE_TIME BIGINT, PREV_FIRE_TIME BIGINT, PRIORITY INTEGER, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT NOT NULL, END_TIME BIGINT, CALENDAR_NAME VARCHAR(60), MISFIRE_INSTR SMALLINT, JOB_DATA BLOB, CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT FK_QRTZ_TRIGGERS_1 FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(60) NOT NULL, TRIGGER_GROUP VARCHAR(60) NOT NULL, REPEAT_COUNT BIGINT NOT NULL, REPEAT_INTERVAL BIGINT NOT NULL, TIMES_TRIGGERED BIGINT NOT NULL, CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_1 FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_1 FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(60) NOT NULL, TRIGGER_GROUP VARCHAR(60) NOT NULL, CRON_EXPRESSION VARCHAR(120) NOT NULL, TIME_ZONE_ID VARCHAR(60), CONSTRAINT PK_QRTZ_SIMPLE_TRG PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT FK_QRTZ_SIMPLE_TRG_1 FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(60) NOT NULL, TRIGGER_GROUP VARCHAR(60) NOT NULL, BLOB_DATA BLOB, CONSTRAINT PK_QRTZ_BLOB_TRIGGERS PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT FK_QRTZ_BLOB_TRIGGERS_1 FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(60) NOT NULL, CALENDAR BLOB NOT NULL, CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(60) NOT NULL, CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(60) NOT NULL, TRIGGER_GROUP VARCHAR(60) NOT NULL, INSTANCE_NAME VARCHAR(80) NOT NULL, FIRED_TIME BIGINT NOT NULL, SCHED_TIME BIGINT NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(60), JOB_GROUP VARCHAR(60), IS_NONCONCURRENT VARCHAR(1), REQUESTS_RECOVERY VARCHAR(1), CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(80) NOT NULL, LAST_CHECKIN_TIME BIGINT NOT NULL, CHECKIN_INTERVAL BIGINT NOT NULL, CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); COMMIT; ================================================ FILE: demo-task-quartz/init/dbTables/tables_h2.sql ================================================ -- Thanks toir Kibbar and Peter Rietzler for contributing the schema for H2 database, -- and verifying that it works with Quartz's StdJDBCDelegate -- -- Note, Quartz depends on row-level locking which means you must use the MVCC=TRUE -- setting on your H2 database, or you will experience dead-locks -- -- -- In your Quartz properties file, you'll need to set -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR (200) NOT NULL , CALENDAR IMAGE NOT NULL ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR (200) NOT NULL , TRIGGER_GROUP VARCHAR (200) NOT NULL , CRON_EXPRESSION VARCHAR (120) NOT NULL , TIME_ZONE_ID VARCHAR (80) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR (95) NOT NULL , TRIGGER_NAME VARCHAR (200) NOT NULL , TRIGGER_GROUP VARCHAR (200) NOT NULL , INSTANCE_NAME VARCHAR (200) NOT NULL , FIRED_TIME BIGINT NOT NULL , SCHED_TIME BIGINT NOT NULL , PRIORITY INTEGER NOT NULL , STATE VARCHAR (16) NOT NULL, JOB_NAME VARCHAR (200) NULL , JOB_GROUP VARCHAR (200) NULL , IS_NONCONCURRENT BOOLEAN NULL , REQUESTS_RECOVERY BOOLEAN NULL ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR (200) NOT NULL ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR (200) NOT NULL , LAST_CHECKIN_TIME BIGINT NOT NULL , CHECKIN_INTERVAL BIGINT NOT NULL ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR (40) NOT NULL ); CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR (200) NOT NULL , JOB_GROUP VARCHAR (200) NOT NULL , DESCRIPTION VARCHAR (250) NULL , JOB_CLASS_NAME VARCHAR (250) NOT NULL , IS_DURABLE BOOLEAN NOT NULL , IS_NONCONCURRENT BOOLEAN NOT NULL , IS_UPDATE_DATA BOOLEAN NOT NULL , REQUESTS_RECOVERY BOOLEAN NOT NULL , JOB_DATA IMAGE NULL ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR (200) NOT NULL , TRIGGER_GROUP VARCHAR (200) NOT NULL , REPEAT_COUNT BIGINT NOT NULL , REPEAT_INTERVAL BIGINT NOT NULL , TIMES_TRIGGERED BIGINT NOT NULL ); CREATE TABLE qrtz_simprop_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INTEGER NULL, INT_PROP_2 INTEGER NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 BOOLEAN NULL, BOOL_PROP_2 BOOLEAN NULL, ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR (200) NOT NULL , TRIGGER_GROUP VARCHAR (200) NOT NULL , BLOB_DATA IMAGE NULL ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR (200) NOT NULL , TRIGGER_GROUP VARCHAR (200) NOT NULL , JOB_NAME VARCHAR (200) NOT NULL , JOB_GROUP VARCHAR (200) NOT NULL , DESCRIPTION VARCHAR (250) NULL , NEXT_FIRE_TIME BIGINT NULL , PREV_FIRE_TIME BIGINT NULL , PRIORITY INTEGER NULL , TRIGGER_STATE VARCHAR (16) NOT NULL , TRIGGER_TYPE VARCHAR (8) NOT NULL , START_TIME BIGINT NOT NULL , END_TIME BIGINT NULL , CALENDAR_NAME VARCHAR (200) NULL , MISFIRE_INSTR SMALLINT NULL , JOB_DATA IMAGE NULL ); ALTER TABLE QRTZ_CALENDARS ADD CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY ( SCHED_NAME, CALENDAR_NAME ); ALTER TABLE QRTZ_CRON_TRIGGERS ADD CONSTRAINT PK_QRTZ_CRON_TRIGGERS PRIMARY KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ); ALTER TABLE QRTZ_FIRED_TRIGGERS ADD CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY ( SCHED_NAME, ENTRY_ID ); ALTER TABLE QRTZ_PAUSED_TRIGGER_GRPS ADD CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY ( SCHED_NAME, TRIGGER_GROUP ); ALTER TABLE QRTZ_SCHEDULER_STATE ADD CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY ( SCHED_NAME, INSTANCE_NAME ); ALTER TABLE QRTZ_LOCKS ADD CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY ( SCHED_NAME, LOCK_NAME ); ALTER TABLE QRTZ_JOB_DETAILS ADD CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY ( SCHED_NAME, JOB_NAME, JOB_GROUP ); ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ); ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ); ALTER TABLE QRTZ_TRIGGERS ADD CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ); ALTER TABLE QRTZ_CRON_TRIGGERS ADD CONSTRAINT FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ) REFERENCES QRTZ_TRIGGERS ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ) ON DELETE CASCADE; ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ) REFERENCES QRTZ_TRIGGERS ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ) ON DELETE CASCADE; ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ) REFERENCES QRTZ_TRIGGERS ( SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP ) ON DELETE CASCADE; ALTER TABLE QRTZ_TRIGGERS ADD CONSTRAINT FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS FOREIGN KEY ( SCHED_NAME, JOB_NAME, JOB_GROUP ) REFERENCES QRTZ_JOB_DETAILS ( SCHED_NAME, JOB_NAME, JOB_GROUP ); COMMIT; ================================================ FILE: demo-task-quartz/init/dbTables/tables_hsqldb.sql ================================================ -- -- In your Quartz properties file, you'll need to set -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.HSQLDBDelegate -- DROP TABLE qrtz_locks IF EXISTS; DROP TABLE qrtz_scheduler_state IF EXISTS; DROP TABLE qrtz_fired_triggers IF EXISTS; DROP TABLE qrtz_paused_trigger_grps IF EXISTS; DROP TABLE qrtz_calendars IF EXISTS; DROP TABLE qrtz_blob_triggers IF EXISTS; DROP TABLE qrtz_cron_triggers IF EXISTS; DROP TABLE qrtz_simple_triggers IF EXISTS; DROP TABLE qrtz_simprop_triggers IF EXISTS; DROP TABLE qrtz_triggers IF EXISTS; DROP TABLE qrtz_job_details IF EXISTS; CREATE TABLE qrtz_job_details ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(250) NOT NULL, IS_DURABLE BOOLEAN NOT NULL, IS_NONCONCURRENT BOOLEAN NOT NULL, IS_UPDATE_DATA BOOLEAN NOT NULL, REQUESTS_RECOVERY BOOLEAN NOT NULL, JOB_DATA BINARY NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME NUMERIC(13) NULL, PREV_FIRE_TIME NUMERIC(13) NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME NUMERIC(13) NOT NULL, END_TIME NUMERIC(13) NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR NUMERIC(2) NULL, JOB_DATA BINARY NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_simple_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT NUMERIC(7) NOT NULL, REPEAT_INTERVAL NUMERIC(12) NOT NULL, TIMES_TRIGGERED NUMERIC(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_cron_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(120) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_simprop_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 NUMERIC(9) NULL, INT_PROP_2 NUMERIC(9) NULL, LONG_PROP_1 NUMERIC(13) NULL, LONG_PROP_2 NUMERIC(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 BOOLEAN NULL, BOOL_PROP_2 BOOLEAN NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_blob_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BINARY NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_calendars ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BINARY NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE qrtz_paused_trigger_grps ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_fired_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME NUMERIC(13) NOT NULL, SCHED_TIME NUMERIC(13) NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT BOOLEAN NULL, REQUESTS_RECOVERY BOOLEAN NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE qrtz_scheduler_state ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME NUMERIC(13) NOT NULL, CHECKIN_INTERVAL NUMERIC(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE qrtz_locks ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); ================================================ FILE: demo-task-quartz/init/dbTables/tables_hsqldb_old.sql ================================================ # # Thanks to Joseph Wilkicki for submitting this file's contents # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.HSQLDBDelegate # # Some users report the need to change the fields # with datatype "OTHER" to datatype "BINARY" with # particular versions (e.g. 1.7.1) of HSQLDB # CREATE TABLE qrtz_job_details ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME LONGVARCHAR(80) NOT NULL, JOB_GROUP LONGVARCHAR(80) NOT NULL, DESCRIPTION LONGVARCHAR(120) NULL, JOB_CLASS_NAME LONGVARCHAR(128) NOT NULL, IS_DURABLE LONGVARCHAR(1) NOT NULL, IS_NONCONCURRENT LONGVARCHAR(1) NOT NULL, IS_UPDATE_DATA LONGVARCHAR(1) NOT NULL, REQUESTS_RECOVERY LONGVARCHAR(1) NOT NULL, JOB_DATA OTHER NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME LONGVARCHAR(80) NOT NULL, TRIGGER_GROUP LONGVARCHAR(80) NOT NULL, JOB_NAME LONGVARCHAR(80) NOT NULL, JOB_GROUP LONGVARCHAR(80) NOT NULL, DESCRIPTION LONGVARCHAR(120) NULL, NEXT_FIRE_TIME NUMERIC(13) NULL, PREV_FIRE_TIME NUMERIC(13) NULL, PRIORITY INTEGER NULL, TRIGGER_STATE LONGVARCHAR(16) NOT NULL, TRIGGER_TYPE LONGVARCHAR(8) NOT NULL, START_TIME NUMERIC(13) NOT NULL, END_TIME NUMERIC(13) NULL, CALENDAR_NAME LONGVARCHAR(80) NULL, MISFIRE_INSTR NUMERIC(2) NULL, JOB_DATA OTHER NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_simple_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME LONGVARCHAR(80) NOT NULL, TRIGGER_GROUP LONGVARCHAR(80) NOT NULL, REPEAT_COUNT NUMERIC(7) NOT NULL, REPEAT_INTERVAL NUMERIC(12) NOT NULL, TIMES_TRIGGERED NUMERIC(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_cron_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME LONGVARCHAR(80) NOT NULL, TRIGGER_GROUP LONGVARCHAR(80) NOT NULL, CRON_EXPRESSION LONGVARCHAR(120) NOT NULL, TIME_ZONE_ID LONGVARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_simprop_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME LONGVARCHAR(200) NOT NULL, TRIGGER_GROUP LONGVARCHAR(200) NOT NULL, STR_PROP_1 LONGVARCHAR(512) NULL, STR_PROP_2 LONGVARCHAR(512) NULL, STR_PROP_3 LONGVARCHAR(512) NULL, INT_PROP_1 NUMERIC(9) NULL, INT_PROP_2 NUMERIC(9) NULL, LONG_PROP_1 NUMERIC(13) NULL, LONG_PROP_2 NUMERIC(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 LONGVARCHAR(1) NULL, BOOL_PROP_2 LONGVARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_blob_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME LONGVARCHAR(80) NOT NULL, TRIGGER_GROUP LONGVARCHAR(80) NOT NULL, BLOB_DATA OTHER NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_calendars ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME LONGVARCHAR(80) NOT NULL, CALENDAR OTHER NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE qrtz_paused_trigger_grps ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP LONGVARCHAR(80) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_fired_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID LONGVARCHAR(95) NOT NULL, TRIGGER_NAME LONGVARCHAR(80) NOT NULL, TRIGGER_GROUP LONGVARCHAR(80) NOT NULL, INSTANCE_NAME LONGVARCHAR(80) NOT NULL, FIRED_TIME NUMERIC(13) NOT NULL, SCHED_TIME NUMERIC(13) NOT NULL, PRIORITY INTEGER NOT NULL, STATE LONGVARCHAR(16) NOT NULL, JOB_NAME LONGVARCHAR(80) NULL, JOB_GROUP LONGVARCHAR(80) NULL, IS_NONCONCURRENT LONGVARCHAR(1) NULL, REQUESTS_RECOVERY LONGVARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE qrtz_scheduler_state ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME LONGVARCHAR(80) NOT NULL, LAST_CHECKIN_TIME NUMERIC(13) NOT NULL, CHECKIN_INTERVAL NUMERIC(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE qrtz_locks ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME LONGVARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_informix.sql ================================================ { } { Thanks to Keith Chew for submitting this. } { } { use the StdJDBCDelegate with Informix. } { } { note that Informix has a 18 cahracter limit on the table name, so the prefix had to be shortened to "q" instread of "qrtz_" } CREATE TABLE qblob_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME varchar(80) NOT NULL, TRIGGER_GROUP varchar(80) NOT NULL, BLOB_DATA byte in table ); ALTER TABLE qblob_triggers ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP); CREATE TABLE qcalendars ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME varchar(80) NOT NULL, CALENDAR byte in table NOT NULL ); ALTER TABLE qcalendars ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,CALENDAR_NAME); CREATE TABLE qcron_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME varchar(80) NOT NULL, TRIGGER_GROUP varchar(80) NOT NULL, CRON_EXPRESSION varchar(120) NOT NULL, TIME_ZONE_ID varchar(80) ); ALTER TABLE qcron_triggers ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP); CREATE TABLE qfired_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID varchar(95) NOT NULL, TRIGGER_NAME varchar(80) NOT NULL, TRIGGER_GROUP varchar(80) NOT NULL, INSTANCE_NAME varchar(80) NOT NULL, FIRED_TIME numeric(13) NOT NULL, SCHED_TIME numeric(13) NOT NULL, PRIORITY integer NOT NULL, STATE varchar(16) NOT NULL, JOB_NAME varchar(80), JOB_GROUP varchar(80), IS_NONCONCURRENT varchar(1), REQUESTS_RECOVERY varchar(1) ); ALTER TABLE qfired_triggers ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,ENTRY_ID); CREATE TABLE qpaused_trigger_grps ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP varchar(80) NOT NULL ); ALTER TABLE qpaused_trigger_grps ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP); CREATE TABLE qscheduler_state ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME varchar(80) NOT NULL, LAST_CHECKIN_TIME numeric(13) NOT NULL, CHECKIN_INTERVAL numeric(13) NOT NULL ); ALTER TABLE qscheduler_state ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,INSTANCE_NAME); CREATE TABLE qlocks ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME varchar(40) NOT NULL ); ALTER TABLE qlocks ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,LOCK_NAME); CREATE TABLE qjob_details ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME varchar(80) NOT NULL, JOB_GROUP varchar(80) NOT NULL, DESCRIPTION varchar(120), JOB_CLASS_NAME varchar(128) NOT NULL, IS_DURABLE varchar(1) NOT NULL, IS_NONCONCURRENT varchar(1) NOT NULL, IS_UPDATE_DATA varchar(1) NOT NULL, REQUESTS_RECOVERY varchar(1) NOT NULL, JOB_DATA byte in table ); ALTER TABLE qjob_details ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,JOB_NAME, JOB_GROUP); CREATE TABLE qsimple_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME varchar(80) NOT NULL, TRIGGER_GROUP varchar(80) NOT NULL, REPEAT_COUNT numeric(7) NOT NULL, REPEAT_INTERVAL numeric(12) NOT NULL, TIMES_TRIGGERED numeric(10) NOT NULL ); ALTER TABLE qsimple_triggers ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP); CREATE TABLE qsimprop_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 NUMERIC(9) NULL, INT_PROP_2 NUMERIC(9) NULL, LONG_PROP_1 NUMERIC(13) NULL, LONG_PROP_2 NUMERIC(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, ); ALTER TABLE qsimprop_triggers ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP); CREATE TABLE qtriggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME varchar(80) NOT NULL, TRIGGER_GROUP varchar(80) NOT NULL, JOB_NAME varchar(80) NOT NULL, JOB_GROUP varchar(80) NOT NULL, DESCRIPTION varchar(120), NEXT_FIRE_TIME numeric(13), PREV_FIRE_TIME numeric(13), PRIORITY integer, TRIGGER_STATE varchar(16) NOT NULL, TRIGGER_TYPE varchar(8) NOT NULL, START_TIME numeric(13) NOT NULL, END_TIME numeric(13), CALENDAR_NAME varchar(80), MISFIRE_INSTR numeric(2), JOB_DATA byte in table ); ALTER TABLE qtriggers ADD CONSTRAINT PRIMARY KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP); ALTER TABLE qblob_triggers ADD CONSTRAINT FOREIGN KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) REFERENCES qtriggers; ALTER TABLE qcron_triggers ADD CONSTRAINT FOREIGN KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) REFERENCES qtriggers; ALTER TABLE qsimple_triggers ADD CONSTRAINT FOREIGN KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) REFERENCES qtriggers; ALTER TABLE qsimprop_triggers ADD CONSTRAINT FOREIGN KEY (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) REFERENCES qtriggers; ALTER TABLE qtriggers ADD CONSTRAINT FOREIGN KEY (SCHED_NAME,JOB_NAME, JOB_GROUP) REFERENCES qjob_details; ================================================ FILE: demo-task-quartz/init/dbTables/tables_mysql.sql ================================================ # # Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar # # PLEASE consider using mysql with innodb tables to avoid locking issues # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; DROP TABLE IF EXISTS QRTZ_LOCKS; DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; DROP TABLE IF EXISTS QRTZ_TRIGGERS; DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; DROP TABLE IF EXISTS QRTZ_CALENDARS; CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(250) NOT NULL, IS_DURABLE VARCHAR(1) NOT NULL, IS_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME BIGINT(13) NULL, PREV_FIRE_TIME BIGINT(13) NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT(13) NOT NULL, END_TIME BIGINT(13) NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR SMALLINT(2) NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT BIGINT(7) NOT NULL, REPEAT_INTERVAL BIGINT(12) NOT NULL, TIMES_TRIGGERED BIGINT(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(200) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BLOB NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME BIGINT(13) NOT NULL, SCHED_TIME BIGINT(13) NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT VARCHAR(1) NULL, REQUESTS_RECOVERY VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME BIGINT(13) NOT NULL, CHECKIN_INTERVAL BIGINT(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_mysql_innodb.sql ================================================ # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # # # By: Ron Cordell - roncordell # I didn't see this anywhere, so I thought I'd post it here. This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM. DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; DROP TABLE IF EXISTS QRTZ_LOCKS; DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; DROP TABLE IF EXISTS QRTZ_TRIGGERS; DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; DROP TABLE IF EXISTS QRTZ_CALENDARS; CREATE TABLE QRTZ_JOB_DETAILS( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(250) NOT NULL, IS_DURABLE VARCHAR(1) NOT NULL, IS_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME BIGINT(13) NULL, PREV_FIRE_TIME BIGINT(13) NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT(13) NOT NULL, END_TIME BIGINT(13) NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR SMALLINT(2) NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT BIGINT(7) NOT NULL, REPEAT_INTERVAL BIGINT(12) NOT NULL, TIMES_TRIGGERED BIGINT(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(120) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BLOB NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)) ENGINE=InnoDB; CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)) ENGINE=InnoDB; CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME BIGINT(13) NOT NULL, SCHED_TIME BIGINT(13) NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT VARCHAR(1) NULL, REQUESTS_RECOVERY VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID)) ENGINE=InnoDB; CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME BIGINT(13) NOT NULL, CHECKIN_INTERVAL BIGINT(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)) ENGINE=InnoDB; CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME)) ENGINE=InnoDB; CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY); CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME); CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME); CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME); CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP); CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_oracle.sql ================================================ -- -- A hint submitted by a user: Oracle DB MUST be created as "shared" and the -- job_queue_processes parameter must be greater than 2 -- However, these settings are pretty much standard after any -- Oracle install, so most users need not worry about this. -- -- Many other users (including the primary author of Quartz) have had success -- runing in dedicated mode, so only consider the above as a hint ;-) -- delete from qrtz_fired_triggers; delete from qrtz_simple_triggers; delete from qrtz_simprop_triggers; delete from qrtz_cron_triggers; delete from qrtz_blob_triggers; delete from qrtz_triggers; delete from qrtz_job_details; delete from qrtz_calendars; delete from qrtz_paused_trigger_grps; delete from qrtz_locks; delete from qrtz_scheduler_state; drop table qrtz_calendars; drop table qrtz_fired_triggers; drop table qrtz_blob_triggers; drop table qrtz_cron_triggers; drop table qrtz_simple_triggers; drop table qrtz_simprop_triggers; drop table qrtz_triggers; drop table qrtz_job_details; drop table qrtz_paused_trigger_grps; drop table qrtz_locks; drop table qrtz_scheduler_state; CREATE TABLE qrtz_job_details ( SCHED_NAME VARCHAR2(120) NOT NULL, JOB_NAME VARCHAR2(200) NOT NULL, JOB_GROUP VARCHAR2(200) NOT NULL, DESCRIPTION VARCHAR2(250) NULL, JOB_CLASS_NAME VARCHAR2(250) NOT NULL, IS_DURABLE VARCHAR2(1) NOT NULL, IS_NONCONCURRENT VARCHAR2(1) NOT NULL, IS_UPDATE_DATA VARCHAR2(1) NOT NULL, REQUESTS_RECOVERY VARCHAR2(1) NOT NULL, JOB_DATA BLOB NULL, CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_triggers ( SCHED_NAME VARCHAR2(120) NOT NULL, TRIGGER_NAME VARCHAR2(200) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, JOB_NAME VARCHAR2(200) NOT NULL, JOB_GROUP VARCHAR2(200) NOT NULL, DESCRIPTION VARCHAR2(250) NULL, NEXT_FIRE_TIME NUMBER(13) NULL, PREV_FIRE_TIME NUMBER(13) NULL, PRIORITY NUMBER(13) NULL, TRIGGER_STATE VARCHAR2(16) NOT NULL, TRIGGER_TYPE VARCHAR2(8) NOT NULL, START_TIME NUMBER(13) NOT NULL, END_TIME NUMBER(13) NULL, CALENDAR_NAME VARCHAR2(200) NULL, MISFIRE_INSTR NUMBER(2) NULL, JOB_DATA BLOB NULL, CONSTRAINT QRTZ_TRIGGERS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT QRTZ_TRIGGER_TO_JOBS_FK FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_simple_triggers ( SCHED_NAME VARCHAR2(120) NOT NULL, TRIGGER_NAME VARCHAR2(200) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, REPEAT_COUNT NUMBER(7) NOT NULL, REPEAT_INTERVAL NUMBER(12) NOT NULL, TIMES_TRIGGERED NUMBER(10) NOT NULL, CONSTRAINT QRTZ_SIMPLE_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT QRTZ_SIMPLE_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_cron_triggers ( SCHED_NAME VARCHAR2(120) NOT NULL, TRIGGER_NAME VARCHAR2(200) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, CRON_EXPRESSION VARCHAR2(120) NOT NULL, TIME_ZONE_ID VARCHAR2(80), CONSTRAINT QRTZ_CRON_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT QRTZ_CRON_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_simprop_triggers ( SCHED_NAME VARCHAR2(120) NOT NULL, TRIGGER_NAME VARCHAR2(200) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, STR_PROP_1 VARCHAR2(512) NULL, STR_PROP_2 VARCHAR2(512) NULL, STR_PROP_3 VARCHAR2(512) NULL, INT_PROP_1 NUMBER(10) NULL, INT_PROP_2 NUMBER(10) NULL, LONG_PROP_1 NUMBER(13) NULL, LONG_PROP_2 NUMBER(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR2(1) NULL, BOOL_PROP_2 VARCHAR2(1) NULL, CONSTRAINT QRTZ_SIMPROP_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT QRTZ_SIMPROP_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_blob_triggers ( SCHED_NAME VARCHAR2(120) NOT NULL, TRIGGER_NAME VARCHAR2(200) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, BLOB_DATA BLOB NULL, CONSTRAINT QRTZ_BLOB_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), CONSTRAINT QRTZ_BLOB_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_calendars ( SCHED_NAME VARCHAR2(120) NOT NULL, CALENDAR_NAME VARCHAR2(200) NOT NULL, CALENDAR BLOB NOT NULL, CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE qrtz_paused_trigger_grps ( SCHED_NAME VARCHAR2(120) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_fired_triggers ( SCHED_NAME VARCHAR2(120) NOT NULL, ENTRY_ID VARCHAR2(95) NOT NULL, TRIGGER_NAME VARCHAR2(200) NOT NULL, TRIGGER_GROUP VARCHAR2(200) NOT NULL, INSTANCE_NAME VARCHAR2(200) NOT NULL, FIRED_TIME NUMBER(13) NOT NULL, SCHED_TIME NUMBER(13) NOT NULL, PRIORITY NUMBER(13) NOT NULL, STATE VARCHAR2(16) NOT NULL, JOB_NAME VARCHAR2(200) NULL, JOB_GROUP VARCHAR2(200) NULL, IS_NONCONCURRENT VARCHAR2(1) NULL, REQUESTS_RECOVERY VARCHAR2(1) NULL, CONSTRAINT QRTZ_FIRED_TRIGGER_PK PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE qrtz_scheduler_state ( SCHED_NAME VARCHAR2(120) NOT NULL, INSTANCE_NAME VARCHAR2(200) NOT NULL, LAST_CHECKIN_TIME NUMBER(13) NOT NULL, CHECKIN_INTERVAL NUMBER(13) NOT NULL, CONSTRAINT QRTZ_SCHEDULER_STATE_PK PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE qrtz_locks ( SCHED_NAME VARCHAR2(120) NOT NULL, LOCK_NAME VARCHAR2(40) NOT NULL, CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY); create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP); create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP); create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP); create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME); create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP); create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE); create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME); create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME); create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP); create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP); create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP); ================================================ FILE: demo-task-quartz/init/dbTables/tables_pointbase.sql ================================================ # # Thanks to Gregg Freeman # # # ...you may want to change defined the size of the "blob" columns before # creating the tables (particularly for the qrtz_job_details.job_data column), # if you will be storing largeounts of data in them # # delete from qrtz_fired_triggers; delete from qrtz_simple_triggers; delete from qrtz_simprop_triggers; delete from qrtz_cron_triggers; delete from qrtz_blob_triggers; delete from qrtz_triggers; delete from qrtz_job_details; delete from qrtz_calendars; delete from qrtz_paused_trigger_grps; delete from qrtz_locks; delete from qrtz_scheduler_state; drop table qrtz_calendars; drop table qrtz_fired_triggers; drop table qrtz_blob_triggers; drop table qrtz_cron_triggers; drop table qrtz_simple_triggers; drop table qrtz_simprop_triggers; drop table qrtz_triggers; drop table qrtz_job_details; drop table qrtz_paused_trigger_grps; drop table qrtz_locks; drop table qrtz_scheduler_state; CREATE TABLE qrtz_job_details ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR2(80) NOT NULL, JOB_GROUP VARCHAR2(80) NOT NULL, DESCRIPTION VARCHAR2(120) NULL, JOB_CLASS_NAME VARCHAR2(128) NOT NULL, IS_DURABLE BOOLEAN NOT NULL, IS_NONCONCURRENT BOOLEAN NOT NULL, IS_UPDATE_DATA BOOLEAN NOT NULL, REQUESTS_RECOVERY BOOLEAN NOT NULL, JOB_DATA BLOB(4K) NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR2(80) NOT NULL, TRIGGER_GROUP VARCHAR2(80) NOT NULL, JOB_NAME VARCHAR2(80) NOT NULL, JOB_GROUP VARCHAR2(80) NOT NULL, DESCRIPTION VARCHAR2(120) NULL, NEXT_FIRE_TIME NUMBER(13) NULL, PREV_FIRE_TIME NUMBER(13) NULL, PRIORITY NUMBER(13) NULL, TRIGGER_STATE VARCHAR2(16) NOT NULL, TRIGGER_TYPE VARCHAR2(8) NOT NULL, START_TIME NUMBER(13) NOT NULL, END_TIME NUMBER(13) NULL, CALENDAR_NAME VARCHAR2(80) NULL, MISFIRE_INSTR NUMBER(2) NULL, JOB_DATA BLOB(4K) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_simple_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR2(80) NOT NULL, TRIGGER_GROUP VARCHAR2(80) NOT NULL, REPEAT_COUNT NUMBER(7) NOT NULL, REPEAT_INTERVAL NUMBER(12) NOT NULL, TIMES_TRIGGERED NUMBER(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_simprop_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 NUMBER(10) NULL, INT_PROP_2 NUMBER(10) NULL, LONG_PROP_1 NUMBER(13) NULL, LONG_PROP_2 NUMBER(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 BOOLEAN NULL, BOOL_PROP_2 BOOLEAN NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_cron_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR2(80) NOT NULL, TRIGGER_GROUP VARCHAR2(80) NOT NULL, CRON_EXPRESSION VARCHAR2(120) NOT NULL, TIME_ZONE_ID VARCHAR2(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_blob_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR2(80) NOT NULL, TRIGGER_GROUP VARCHAR2(80) NOT NULL, BLOB_DATA BLOB(4K) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_calendars ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR2(80) NOT NULL, CALENDAR BLOB(4K) NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE qrtz_paused_trigger_grps ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR2(80) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_fired_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR2(95) NOT NULL, TRIGGER_NAME VARCHAR2(80) NOT NULL, TRIGGER_GROUP VARCHAR2(80) NOT NULL, INSTANCE_NAME VARCHAR2(80) NOT NULL, FIRED_TIME NUMBER(13) NOT NULL, SCHED_TIME NUMBER(13) NOT NULL, PRIORITY NUMBER(13) NOT NULL, STATE VARCHAR2(16) NOT NULL, JOB_NAME VARCHAR2(80) NULL, JOB_GROUP VARCHAR2(80) NULL, IS_NONCONCURRENT BOOLEAN NULL, REQUESTS_RECOVERY BOOLEAN NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE qrtz_scheduler_state ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR2(80) NOT NULL, LAST_CHECKIN_TIME NUMBER(13) NOT NULL, CHECKIN_INTERVAL NUMBER(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE qrtz_locks ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR2(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_postgres.sql ================================================ -- Thanks to Patrick Lightbody for submitting this... -- -- In your Quartz properties file, you'll need to set -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate drop table qrtz_fired_triggers; DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE QRTZ_SCHEDULER_STATE; DROP TABLE QRTZ_LOCKS; drop table qrtz_simple_triggers; drop table qrtz_cron_triggers; drop table qrtz_simprop_triggers; DROP TABLE QRTZ_BLOB_TRIGGERS; drop table qrtz_triggers; drop table qrtz_job_details; drop table qrtz_calendars; CREATE TABLE qrtz_job_details ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(250) NOT NULL, IS_DURABLE BOOL NOT NULL, IS_NONCONCURRENT BOOL NOT NULL, IS_UPDATE_DATA BOOL NOT NULL, REQUESTS_RECOVERY BOOL NOT NULL, JOB_DATA BYTEA NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME BIGINT NULL, PREV_FIRE_TIME BIGINT NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT NOT NULL, END_TIME BIGINT NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR SMALLINT NULL, JOB_DATA BYTEA NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE qrtz_simple_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT BIGINT NOT NULL, REPEAT_INTERVAL BIGINT NOT NULL, TIMES_TRIGGERED BIGINT NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_cron_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(120) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_simprop_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 BOOL NULL, BOOL_PROP_2 BOOL NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_blob_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BYTEA NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_calendars ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BYTEA NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE qrtz_paused_trigger_grps ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE qrtz_fired_triggers ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME BIGINT NOT NULL, SCHED_TIME BIGINT NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT BOOL NULL, REQUESTS_RECOVERY BOOL NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE qrtz_scheduler_state ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME BIGINT NOT NULL, CHECKIN_INTERVAL BIGINT NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE qrtz_locks ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY); create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP); create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP); create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP); create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME); create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP); create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE); create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME); create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME); create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP); create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP); create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_sapdb.sql ================================================ # # Thanks to Andrew Perepelytsya for submitting this file. # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(128) NOT NULL, IS_DURABLE VARCHAR(1) NOT NULL, IS_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA LONG BYTE NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME FIXED(13) NULL, PREV_FIRE_TIME FIXED(13) NULL, PRIORITY FIXED(13) NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME FIXED(13) NOT NULL, END_TIME FIXED(13) NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR FIXED(2) NULL, JOB_DATA LONG BYTE NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT FIXED(7) NOT NULL, REPEAT_INTERVAL FIXED(12) NOT NULL, TIMES_TRIGGERED FIXED(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 FIXED(10) NULL, INT_PROP_2 FIXED(10) NULL, LONG_PROP_1 FIXED(13) NULL, LONG_PROP_2 FIXED(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(120) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA LONG BYTE NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, CALENDAR LONG BYTE NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME FIXED(13) NOT NULL, SCHED_TIME FIXED(13) NOT NULL, PRIORITY FIXED(13) NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT VARCHAR(1) NULL, REQUESTS_RECOVERY VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME FIXED(13) NOT NULL, CHECKIN_INTERVAL FIXED(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); commit; ================================================ FILE: demo-task-quartz/init/dbTables/tables_solid.sql ================================================ DROP TABLE qrtz_locks; DROP TABLE qrtz_scheduler_state; DROP TABLE qrtz_fired_triggers; DROP TABLE qrtz_paused_trigger_grps; DROP TABLE qrtz_calendars; DROP TABLE qrtz_blob_triggers; DROP TABLE qrtz_cron_triggers; DROP TABLE qrtz_simple_triggers; DROP TABLE qrtz_simprop_triggers; DROP TABLE qrtz_triggers; DROP TABLE qrtz_job_details; create table qrtz_job_details ( sched_name varchar(120) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120) , job_class_name varchar(128) not null, is_durable varchar(5) not null, is_nonconcurrent varchar(5) not null, is_update_data varchar(5) not null, requests_recovery varchar(5) not null, job_data long varbinary, primary key (sched_name,job_name,job_group) ); create table qrtz_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, job_name varchar(80) not null, job_group varchar(80) not null, description varchar(120) , next_fire_time numeric(13), prev_fire_time numeric(13), priority integer, trigger_state varchar(16) not null, trigger_type varchar(8) not null, start_time numeric(13) not null, end_time numeric(13), calendar_name varchar(80), misfire_instr smallint, job_data long varbinary, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,job_name,job_group) references qrtz_job_details(sched_name,job_name,job_group) ); create table qrtz_simple_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, repeat_count numeric(13) not null, repeat_interval numeric(13) not null, times_triggered numeric(13) not null, primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); CREATE TABLE qrtz_simprop_triggers ( sched_name varchar(120) not null, trigger_name VARCHAR(200) NOT NULL, trigger_group VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INTEGER NULL, INT_PROP_2 INTEGER NULL, LONG_PROP_1 NUMERIC(13) NULL, LONG_PROP_2 NUMERIC(13) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(5) NULL, BOOL_PROP_2 VARCHAR(5) NULL, PRIMARY KEY (sched_name,trigger_name,trigger_group), FOREIGN KEY (sched_name,trigger_name,trigger_group) REFERENCES qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_cron_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, cron_expression varchar(120) not null, time_zone_id varchar(80), primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_blob_triggers( sched_name varchar(120) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, blob_data long varbinary , primary key (sched_name,trigger_name,trigger_group), foreign key (sched_name,trigger_name,trigger_group) references qrtz_triggers(sched_name,trigger_name,trigger_group) ); create table qrtz_calendars( sched_name varchar(120) not null, calendar_name varchar(80) not null, calendar long varbinary not null, primary key (sched_name,calendar_name) ); create table qrtz_paused_trigger_grps ( sched_name varchar(120) not null, trigger_group varchar(80) not null, primary key (sched_name,trigger_group) ); create table qrtz_fired_triggers( sched_name varchar(120) not null, entry_id varchar(95) not null, trigger_name varchar(80) not null, trigger_group varchar(80) not null, instance_name varchar(80) not null, fired_time numeric(13) not null, sched_time numeric(13) not null, priority integer not null, state varchar(16) not null, job_name varchar(80) null, job_group varchar(80) null, is_nonconcurrent varchar(5) null, requests_recovery varchar(5) null, primary key (sched_name,entry_id) ); create table qrtz_scheduler_state ( sched_name varchar(120) not null, instance_name varchar(80) not null, last_checkin_time numeric(13) not null, checkin_interval numeric(13) not null, primary key (sched_name,instance_name) ); create table qrtz_locks ( sched_name varchar(120) not null, lock_name varchar(40) not null, primary key (sched_name,lock_name) ); commit work; ================================================ FILE: demo-task-quartz/init/dbTables/tables_sqlServer.sql ================================================ --# thanks to George Papastamatopoulos for submitting this ... and Marko Lahma for --# updating it. --# --# In your Quartz properties file, you'll need to set --# org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.MSSQLDelegate --# --# you shouse enter your DB instance's name on the next line in place of "enter_db_name_here" --# --# --# From a helpful (but anonymous) Quartz user: --# --# Regarding this error message: --# --# [Microsoft][SQLServer 2000 Driver for JDBC]Can't start a cloned connection while in manual transaction mode. --# --# --# I added "SelectMethod=cursor;" to my Connection URL in the config file. --# It Seems to work, hopefully no side effects. --# --# example: --# "jdbc:microsoft:sqlserver://dbmachine:1433;SelectMethod=cursor"; --# --# Another user has pointed out that you will probably need to use the --# JTDS driver --# USE [enter_db_name_here] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) ALTER TABLE [dbo].[QRTZ_TRIGGERS] DROP CONSTRAINT FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) ALTER TABLE [dbo].[QRTZ_CRON_TRIGGERS] DROP CONSTRAINT FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) ALTER TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] DROP CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) ALTER TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] DROP CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_CALENDARS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_CALENDARS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_CRON_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_CRON_TRIGGERS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_BLOB_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_BLOB_TRIGGERS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_FIRED_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_FIRED_TRIGGERS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_PAUSED_TRIGGER_GRPS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_SCHEDULER_STATE]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_SCHEDULER_STATE] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_LOCKS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_LOCKS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_JOB_DETAILS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_JOB_DETAILS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_SIMPLE_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_SIMPROP_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] GO IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) DROP TABLE [dbo].[QRTZ_TRIGGERS] GO CREATE TABLE [dbo].[QRTZ_CALENDARS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [CALENDAR_NAME] [VARCHAR] (200) NOT NULL , [CALENDAR] [IMAGE] NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_CRON_TRIGGERS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [TRIGGER_NAME] [VARCHAR] (200) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL , [CRON_EXPRESSION] [VARCHAR] (120) NOT NULL , [TIME_ZONE_ID] [VARCHAR] (80) ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_FIRED_TRIGGERS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [ENTRY_ID] [VARCHAR] (95) NOT NULL , [TRIGGER_NAME] [VARCHAR] (200) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL , [INSTANCE_NAME] [VARCHAR] (200) NOT NULL , [FIRED_TIME] [BIGINT] NOT NULL , [SCHED_TIME] [BIGINT] NOT NULL , [PRIORITY] [INTEGER] NOT NULL , [STATE] [VARCHAR] (16) NOT NULL, [JOB_NAME] [VARCHAR] (200) NULL , [JOB_GROUP] [VARCHAR] (200) NULL , [IS_NONCONCURRENT] [VARCHAR] (1) NULL , [REQUESTS_RECOVERY] [VARCHAR] (1) NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_SCHEDULER_STATE] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [INSTANCE_NAME] [VARCHAR] (200) NOT NULL , [LAST_CHECKIN_TIME] [BIGINT] NOT NULL , [CHECKIN_INTERVAL] [BIGINT] NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_LOCKS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [LOCK_NAME] [VARCHAR] (40) NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_JOB_DETAILS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [JOB_NAME] [VARCHAR] (200) NOT NULL , [JOB_GROUP] [VARCHAR] (200) NOT NULL , [DESCRIPTION] [VARCHAR] (250) NULL , [JOB_CLASS_NAME] [VARCHAR] (250) NOT NULL , [IS_DURABLE] [VARCHAR] (1) NOT NULL , [IS_NONCONCURRENT] [VARCHAR] (1) NOT NULL , [IS_UPDATE_DATA] [VARCHAR] (1) NOT NULL , [REQUESTS_RECOVERY] [VARCHAR] (1) NOT NULL , [JOB_DATA] [IMAGE] NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [TRIGGER_NAME] [VARCHAR] (200) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL , [REPEAT_COUNT] [BIGINT] NOT NULL , [REPEAT_INTERVAL] [BIGINT] NOT NULL , [TIMES_TRIGGERED] [BIGINT] NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [TRIGGER_NAME] [VARCHAR] (200) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL , [STR_PROP_1] [VARCHAR] (512) NULL, [STR_PROP_2] [VARCHAR] (512) NULL, [STR_PROP_3] [VARCHAR] (512) NULL, [INT_PROP_1] [INT] NULL, [INT_PROP_2] [INT] NULL, [LONG_PROP_1] [BIGINT] NULL, [LONG_PROP_2] [BIGINT] NULL, [DEC_PROP_1] [NUMERIC] (13,4) NULL, [DEC_PROP_2] [NUMERIC] (13,4) NULL, [BOOL_PROP_1] [VARCHAR] (1) NULL, [BOOL_PROP_2] [VARCHAR] (1) NULL, ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_BLOB_TRIGGERS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [TRIGGER_NAME] [VARCHAR] (200) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL , [BLOB_DATA] [IMAGE] NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[QRTZ_TRIGGERS] ( [SCHED_NAME] [VARCHAR] (120) NOT NULL , [TRIGGER_NAME] [VARCHAR] (200) NOT NULL , [TRIGGER_GROUP] [VARCHAR] (200) NOT NULL , [JOB_NAME] [VARCHAR] (200) NOT NULL , [JOB_GROUP] [VARCHAR] (200) NOT NULL , [DESCRIPTION] [VARCHAR] (250) NULL , [NEXT_FIRE_TIME] [BIGINT] NULL , [PREV_FIRE_TIME] [BIGINT] NULL , [PRIORITY] [INTEGER] NULL , [TRIGGER_STATE] [VARCHAR] (16) NOT NULL , [TRIGGER_TYPE] [VARCHAR] (8) NOT NULL , [START_TIME] [BIGINT] NOT NULL , [END_TIME] [BIGINT] NULL , [CALENDAR_NAME] [VARCHAR] (200) NULL , [MISFIRE_INSTR] [SMALLINT] NULL , [JOB_DATA] [IMAGE] NULL ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_CALENDARS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_CALENDARS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [CALENDAR_NAME] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_CRON_TRIGGERS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_CRON_TRIGGERS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_FIRED_TRIGGERS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_FIRED_TRIGGERS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [ENTRY_ID] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_PAUSED_TRIGGER_GRPS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [TRIGGER_GROUP] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_SCHEDULER_STATE] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_SCHEDULER_STATE] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [INSTANCE_NAME] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_LOCKS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_LOCKS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [LOCK_NAME] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_JOB_DETAILS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_JOB_DETAILS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [JOB_NAME], [JOB_GROUP] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_SIMPLE_TRIGGERS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_SIMPROP_TRIGGERS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_TRIGGERS] WITH NOCHECK ADD CONSTRAINT [PK_QRTZ_TRIGGERS] PRIMARY KEY CLUSTERED ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON [PRIMARY] GO ALTER TABLE [dbo].[QRTZ_CRON_TRIGGERS] ADD CONSTRAINT [FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS] FOREIGN KEY ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) REFERENCES [dbo].[QRTZ_TRIGGERS] ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON DELETE CASCADE GO ALTER TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] ADD CONSTRAINT [FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS] FOREIGN KEY ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) REFERENCES [dbo].[QRTZ_TRIGGERS] ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON DELETE CASCADE GO ALTER TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] ADD CONSTRAINT [FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS] FOREIGN KEY ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) REFERENCES [dbo].[QRTZ_TRIGGERS] ( [SCHED_NAME], [TRIGGER_NAME], [TRIGGER_GROUP] ) ON DELETE CASCADE GO ALTER TABLE [dbo].[QRTZ_TRIGGERS] ADD CONSTRAINT [FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS] FOREIGN KEY ( [SCHED_NAME], [JOB_NAME], [JOB_GROUP] ) REFERENCES [dbo].[QRTZ_JOB_DETAILS] ( [SCHED_NAME], [JOB_NAME], [JOB_GROUP] ) GO ================================================ FILE: demo-task-quartz/init/dbTables/tables_sybase.sql ================================================ /*==============================================================================================*/ /* Quartz database tables creation script for Sybase ASE 12.5 */ /* Written by Pertti Laiho (email: pertti.laiho@deio.net), 9th May 2003 */ /* */ /* Compatible with Quartz version 1.1.2 */ /* */ /* Sybase ASE works ok with the SybaseDelegate delegate class. That means in your Quartz properties */ /* file, you'll need to set: */ /* org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.SybaseDelegate */ /*==============================================================================================*/ use your_db_name_here go /*==============================================================================*/ /* Clear all tables: */ /*==============================================================================*/ IF OBJECT_ID('QRTZ_FIRED_TRIGGERS') IS NOT NULL delete from QRTZ_FIRED_TRIGGERS go IF OBJECT_ID('QRTZ_PAUSED_TRIGGER_GRPS') IS NOT NULL delete from QRTZ_PAUSED_TRIGGER_GRPS go IF OBJECT_ID('QRTZ_SCHEDULER_STATE') IS NOT NULL delete from QRTZ_SCHEDULER_STATE go IF OBJECT_ID('QRTZ_LOCKS') IS NOT NULL delete from QRTZ_LOCKS go IF OBJECT_ID('QRTZ_SIMPLE_TRIGGERS') IS NOT NULL delete from QRTZ_SIMPLE_TRIGGERS go IF OBJECT_ID('QRTZ_SIMPROP_TRIGGERS') IS NOT NULL delete from QRTZ_SIMPROP_TRIGGERS go IF OBJECT_ID('QRTZ_CRON_TRIGGERS') IS NOT NULL delete from QRTZ_CRON_TRIGGERS go IF OBJECT_ID('QRTZ_BLOB_TRIGGERS') IS NOT NULL delete from QRTZ_BLOB_TRIGGERS go IF OBJECT_ID('QRTZ_TRIGGERS') IS NOT NULL delete from QRTZ_TRIGGERS go IF OBJECT_ID('QRTZ_JOB_DETAILS') IS NOT NULL delete from QRTZ_JOB_DETAILS go IF OBJECT_ID('QRTZ_CALENDARS') IS NOT NULL delete from QRTZ_CALENDARS go /*==============================================================================*/ /* Drop constraints: */ /*==============================================================================*/ alter table QRTZ_TRIGGERS drop constraint FK_triggers_job_details go alter table QRTZ_CRON_TRIGGERS drop constraint FK_cron_triggers_triggers go alter table QRTZ_SIMPLE_TRIGGERS drop constraint FK_simple_triggers_triggers go alter table QRTZ_SIMPROP_TRIGGERS drop constraint FK_simprop_triggers_triggers go alter table QRTZ_BLOB_TRIGGERS drop constraint FK_blob_triggers_triggers go /*==============================================================================*/ /* Drop tables: */ /*==============================================================================*/ drop table QRTZ_FIRED_TRIGGERS go drop table QRTZ_PAUSED_TRIGGER_GRPS go drop table QRTZ_SCHEDULER_STATE go drop table QRTZ_LOCKS go drop table QRTZ_SIMPLE_TRIGGERS go drop table QRTZ_SIMPROP_TRIGGERS go drop table QRTZ_CRON_TRIGGERS go drop table QRTZ_BLOB_TRIGGERS go drop table QRTZ_TRIGGERS go drop table QRTZ_JOB_DETAILS go drop table QRTZ_CALENDARS go /*==============================================================================*/ /* Create tables: */ /*==============================================================================*/ create table QRTZ_CALENDARS ( SCHED_NAME varchar(120) not null, CALENDAR_NAME varchar(80) not null, CALENDAR image not null ) go create table QRTZ_CRON_TRIGGERS ( SCHED_NAME varchar(120) not null, TRIGGER_NAME varchar(80) not null, TRIGGER_GROUP varchar(80) not null, CRON_EXPRESSION varchar(120) not null, TIME_ZONE_ID varchar(80) null, ) go create table QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME varchar(120) not null, TRIGGER_GROUP varchar(80) not null, ) go create table QRTZ_FIRED_TRIGGERS( SCHED_NAME varchar(120) not null, ENTRY_ID varchar(95) not null, TRIGGER_NAME varchar(80) not null, TRIGGER_GROUP varchar(80) not null, INSTANCE_NAME varchar(80) not null, FIRED_TIME numeric(13,0) not null, SCHED_TIME numeric(13,0) not null, PRIORITY int not null, STATE varchar(16) not null, JOB_NAME varchar(80) null, JOB_GROUP varchar(80) null, IS_NONCONCURRENT bit not null, REQUESTS_RECOVERY bit not null, ) go create table QRTZ_SCHEDULER_STATE ( SCHED_NAME varchar(120) not null, INSTANCE_NAME varchar(80) not null, LAST_CHECKIN_TIME numeric(13,0) not null, CHECKIN_INTERVAL numeric(13,0) not null, ) go create table QRTZ_LOCKS ( SCHED_NAME varchar(120) not null, LOCK_NAME varchar(40) not null, ) go create table QRTZ_JOB_DETAILS ( SCHED_NAME varchar(120) not null, JOB_NAME varchar(80) not null, JOB_GROUP varchar(80) not null, DESCRIPTION varchar(120) null, JOB_CLASS_NAME varchar(128) not null, IS_DURABLE bit not null, IS_NONCONCURRENT bit not null, IS_UPDATE_DATA bit not null, REQUESTS_RECOVERY bit not null, JOB_DATA image null ) go create table QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME varchar(120) not null, TRIGGER_NAME varchar(80) not null, TRIGGER_GROUP varchar(80) not null, REPEAT_COUNT numeric(13,0) not null, REPEAT_INTERVAL numeric(13,0) not null, TIMES_TRIGGERED numeric(13,0) not null ) go CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 NUMERIC(13,0) NULL, LONG_PROP_2 NUMERIC(13,0) NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 bit NOT NULL, BOOL_PROP_2 bit NOT NULL ) go create table QRTZ_BLOB_TRIGGERS ( SCHED_NAME varchar(120) not null, TRIGGER_NAME varchar(80) not null, TRIGGER_GROUP varchar(80) not null, BLOB_DATA image null ) go create table QRTZ_TRIGGERS ( SCHED_NAME varchar(120) not null, TRIGGER_NAME varchar(80) not null, TRIGGER_GROUP varchar(80) not null, JOB_NAME varchar(80) not null, JOB_GROUP varchar(80) not null, DESCRIPTION varchar(120) null, NEXT_FIRE_TIME numeric(13,0) null, PREV_FIRE_TIME numeric(13,0) null, PRIORITY int null, TRIGGER_STATE varchar(16) not null, TRIGGER_TYPE varchar(8) not null, START_TIME numeric(13,0) not null, END_TIME numeric(13,0) null, CALENDAR_NAME varchar(80) null, MISFIRE_INSTR smallint null, JOB_DATA image null ) go /*==============================================================================*/ /* Create primary key constraints: */ /*==============================================================================*/ alter table QRTZ_CALENDARS add constraint PK_qrtz_calendars primary key clustered (SCHED_NAME,CALENDAR_NAME) go alter table QRTZ_CRON_TRIGGERS add constraint PK_qrtz_cron_triggers primary key clustered (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) go alter table QRTZ_FIRED_TRIGGERS add constraint PK_qrtz_fired_triggers primary key clustered (SCHED_NAME,ENTRY_ID) go alter table QRTZ_PAUSED_TRIGGER_GRPS add constraint PK_qrtz_paused_trigger_grps primary key clustered (SCHED_NAME,TRIGGER_GROUP) go alter table QRTZ_SCHEDULER_STATE add constraint PK_qrtz_scheduler_state primary key clustered (SCHED_NAME,INSTANCE_NAME) go alter table QRTZ_LOCKS add constraint PK_qrtz_locks primary key clustered (SCHED_NAME,LOCK_NAME) go alter table QRTZ_JOB_DETAILS add constraint PK_qrtz_job_details primary key clustered (SCHED_NAME,JOB_NAME, JOB_GROUP) go alter table QRTZ_SIMPLE_TRIGGERS add constraint PK_qrtz_simple_triggers primary key clustered (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) go alter table QRTZ_SIMPROP_TRIGGERS add constraint PK_qrtz_simprop_triggers primary key clustered (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) go alter table QRTZ_TRIGGERS add constraint PK_qrtz_triggers primary key clustered (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) go alter table QRTZ_BLOB_TRIGGERS add constraint PK_qrtz_blob_triggers primary key clustered (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP) go /*==============================================================================*/ /* Create foreign key constraints: */ /*==============================================================================*/ alter table QRTZ_CRON_TRIGGERS add constraint FK_cron_triggers_triggers foreign key (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) references QRTZ_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) go alter table QRTZ_SIMPLE_TRIGGERS add constraint FK_simple_triggers_triggers foreign key (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) references QRTZ_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) go alter table QRTZ_SIMPROP_TRIGGERS add constraint FK_simprop_triggers_triggers foreign key (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) references QRTZ_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) go alter table QRTZ_TRIGGERS add constraint FK_triggers_job_details foreign key (SCHED_NAME,JOB_NAME,JOB_GROUP) references QRTZ_JOB_DETAILS (SCHED_NAME,JOB_NAME,JOB_GROUP) go alter table QRTZ_BLOB_TRIGGERS add constraint FK_blob_triggers_triggers foreign key (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) references QRTZ_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) go /*==============================================================================*/ /* End of script. */ /*==============================================================================*/ ================================================ FILE: demo-task-quartz/pom.xml ================================================ 4.0.0 demo-task-quartz 1.0.0-SNAPSHOT jar demo-task-quartz Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.1.0 1.2.10 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-quartz tk.mybatis mapper-spring-boot-starter ${mybatis.mapper.version} com.github.pagehelper pagehelper-spring-boot-starter ${mybatis.pagehelper.version} mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-task-quartz org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/SpringBootDemoTaskQuartzApplication.java ================================================ package com.xkcoding.task.quartz; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tk.mybatis.spring.annotation.MapperScan; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-11-23 20:33 */ @MapperScan(basePackages = {"com.xkcoding.task.quartz.mapper"}) @SpringBootApplication public class SpringBootDemoTaskQuartzApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTaskQuartzApplication.class, args); } } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/common/ApiResponse.java ================================================ package com.xkcoding.task.quartz.common; import lombok.Data; import org.springframework.http.HttpStatus; import java.io.Serializable; /** *

    * 通用Api封装 *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:59 */ @Data public class ApiResponse implements Serializable { /** * 返回信息 */ private String message; /** * 返回数据 */ private Object data; public ApiResponse() { } private ApiResponse(String message, Object data) { this.message = message; this.data = data; } /** * 通用封装获取ApiResponse对象 * * @param message 返回信息 * @param data 返回数据 * @return ApiResponse */ public static ApiResponse of(String message, Object data) { return new ApiResponse(message, data); } /** * 通用成功封装获取ApiResponse对象 * * @param data 返回数据 * @return ApiResponse */ public static ApiResponse ok(Object data) { return new ApiResponse(HttpStatus.OK.getReasonPhrase(), data); } /** * 通用封装获取ApiResponse对象 * * @param message 返回信息 * @return ApiResponse */ public static ApiResponse msg(String message) { return of(message, null); } } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/controller/JobController.java ================================================ package com.xkcoding.task.quartz.controller; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.github.pagehelper.PageInfo; import com.xkcoding.task.quartz.common.ApiResponse; import com.xkcoding.task.quartz.entity.domain.JobAndTrigger; import com.xkcoding.task.quartz.entity.form.JobForm; import com.xkcoding.task.quartz.service.JobService; import lombok.extern.slf4j.Slf4j; import org.quartz.SchedulerException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** *

    * Job Controller *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:23 */ @RestController @RequestMapping("/job") @Slf4j public class JobController { private final JobService jobService; @Autowired public JobController(JobService jobService) { this.jobService = jobService; } /** * 保存定时任务 */ @PostMapping public ResponseEntity addJob(@Valid JobForm form) { try { jobService.addJob(form); } catch (Exception e) { return new ResponseEntity<>(ApiResponse.msg(e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } return new ResponseEntity<>(ApiResponse.msg("操作成功"), HttpStatus.CREATED); } /** * 删除定时任务 */ @DeleteMapping public ResponseEntity deleteJob(JobForm form) throws SchedulerException { if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) { return new ResponseEntity<>(ApiResponse.msg("参数不能为空"), HttpStatus.BAD_REQUEST); } jobService.deleteJob(form); return new ResponseEntity<>(ApiResponse.msg("删除成功"), HttpStatus.OK); } /** * 暂停定时任务 */ @PutMapping(params = "pause") public ResponseEntity pauseJob(JobForm form) throws SchedulerException { if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) { return new ResponseEntity<>(ApiResponse.msg("参数不能为空"), HttpStatus.BAD_REQUEST); } jobService.pauseJob(form); return new ResponseEntity<>(ApiResponse.msg("暂停成功"), HttpStatus.OK); } /** * 恢复定时任务 */ @PutMapping(params = "resume") public ResponseEntity resumeJob(JobForm form) throws SchedulerException { if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) { return new ResponseEntity<>(ApiResponse.msg("参数不能为空"), HttpStatus.BAD_REQUEST); } jobService.resumeJob(form); return new ResponseEntity<>(ApiResponse.msg("恢复成功"), HttpStatus.OK); } /** * 修改定时任务,定时时间 */ @PutMapping(params = "cron") public ResponseEntity cronJob(@Valid JobForm form) { try { jobService.cronJob(form); } catch (Exception e) { return new ResponseEntity<>(ApiResponse.msg(e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } return new ResponseEntity<>(ApiResponse.msg("修改成功"), HttpStatus.OK); } @GetMapping public ResponseEntity jobList(Integer currentPage, Integer pageSize) { if (ObjectUtil.isNull(currentPage)) { currentPage = 1; } if (ObjectUtil.isNull(pageSize)) { pageSize = 10; } PageInfo all = jobService.list(currentPage, pageSize); return ResponseEntity.ok(ApiResponse.ok(Dict.create().set("total", all.getTotal()).set("data", all.getList()))); } } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/entity/domain/JobAndTrigger.java ================================================ package com.xkcoding.task.quartz.entity.domain; import lombok.Data; import java.math.BigInteger; /** *

    * 实体类 *

    * * @author yangkai.shen * @date Created in 2018-11-26 15:05 */ @Data public class JobAndTrigger { /** * 定时任务名称 */ private String jobName; /** * 定时任务组 */ private String jobGroup; /** * 定时任务全类名 */ private String jobClassName; /** * 触发器名称 */ private String triggerName; /** * 触发器组 */ private String triggerGroup; /** * 重复间隔 */ private BigInteger repeatInterval; /** * 触发次数 */ private BigInteger timesTriggered; /** * cron 表达式 */ private String cronExpression; /** * 时区 */ private String timeZoneId; /** * 定时任务状态 */ private String triggerState; } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/entity/form/JobForm.java ================================================ package com.xkcoding.task.quartz.entity.form; import lombok.Data; import lombok.experimental.Accessors; import javax.validation.constraints.NotBlank; /** *

    * 定时任务详情 *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:42 */ @Data @Accessors(chain = true) public class JobForm { /** * 定时任务全类名 */ @NotBlank(message = "类名不能为空") private String jobClassName; /** * 任务组名 */ @NotBlank(message = "任务组名不能为空") private String jobGroupName; /** * 定时任务cron表达式 */ @NotBlank(message = "cron表达式不能为空") private String cronExpression; } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/job/HelloJob.java ================================================ package com.xkcoding.task.quartz.job; import cn.hutool.core.date.DateUtil; import com.xkcoding.task.quartz.job.base.BaseJob; import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; /** *

    * Hello Job *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:22 */ @Slf4j public class HelloJob implements BaseJob { @Override public void execute(JobExecutionContext context) { log.error("Hello Job 执行时间: {}", DateUtil.now()); } } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/job/TestJob.java ================================================ package com.xkcoding.task.quartz.job; import cn.hutool.core.date.DateUtil; import com.xkcoding.task.quartz.job.base.BaseJob; import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; /** *

    * Test Job *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:22 */ @Slf4j public class TestJob implements BaseJob { @Override public void execute(JobExecutionContext context) { log.error("Test Job 执行时间: {}", DateUtil.now()); } } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/job/base/BaseJob.java ================================================ package com.xkcoding.task.quartz.job.base; import org.quartz.*; /** *

    * Job 基类,主要是在 {@link org.quartz.Job} 上再封装一层,只让我们自己项目里的Job去实现 *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:27 */ public interface BaseJob extends Job { /** *

    * Called by the {@link Scheduler} when a {@link Trigger} * fires that is associated with the Job. *

    * *

    * The implementation may wish to set a * {@link JobExecutionContext#setResult(Object) result} object on the * {@link JobExecutionContext} before this method exits. The result itself * is meaningless to Quartz, but may be informative to * {@link JobListener}s or * {@link TriggerListener}s that are watching the job's * execution. *

    * * @param context 上下文 * @throws JobExecutionException if there is an exception while executing the job. */ @Override void execute(JobExecutionContext context) throws JobExecutionException; } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/mapper/JobMapper.java ================================================ package com.xkcoding.task.quartz.mapper; import com.xkcoding.task.quartz.entity.domain.JobAndTrigger; import org.springframework.stereotype.Component; import java.util.List; /** *

    * Job Mapper *

    * * @author yangkai.shen * @date Created in 2018-11-26 15:12 */ @Component public interface JobMapper { /** * 查询定时作业和触发器列表 * * @return 定时作业和触发器列表 */ List list(); } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/service/JobService.java ================================================ package com.xkcoding.task.quartz.service; import com.github.pagehelper.PageInfo; import com.xkcoding.task.quartz.entity.domain.JobAndTrigger; import com.xkcoding.task.quartz.entity.form.JobForm; import org.quartz.SchedulerException; /** *

    * Job Service *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:24 */ public interface JobService { /** * 添加并启动定时任务 * * @param form 表单参数 {@link JobForm} * @throws Exception 异常 */ void addJob(JobForm form) throws Exception; /** * 删除定时任务 * * @param form 表单参数 {@link JobForm} * @throws SchedulerException 异常 */ void deleteJob(JobForm form) throws SchedulerException; /** * 暂停定时任务 * * @param form 表单参数 {@link JobForm} * @throws SchedulerException 异常 */ void pauseJob(JobForm form) throws SchedulerException; /** * 恢复定时任务 * * @param form 表单参数 {@link JobForm} * @throws SchedulerException 异常 */ void resumeJob(JobForm form) throws SchedulerException; /** * 重新配置定时任务 * * @param form 表单参数 {@link JobForm} * @throws Exception 异常 */ void cronJob(JobForm form) throws Exception; /** * 查询定时任务列表 * * @param currentPage 当前页 * @param pageSize 每页条数 * @return 定时任务列表 */ PageInfo list(Integer currentPage, Integer pageSize); } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/service/impl/JobServiceImpl.java ================================================ package com.xkcoding.task.quartz.service.impl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.xkcoding.task.quartz.entity.domain.JobAndTrigger; import com.xkcoding.task.quartz.entity.form.JobForm; import com.xkcoding.task.quartz.mapper.JobMapper; import com.xkcoding.task.quartz.service.JobService; import com.xkcoding.task.quartz.util.JobUtil; import lombok.extern.slf4j.Slf4j; import org.quartz.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** *

    * Job Service *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:25 */ @Service @Slf4j public class JobServiceImpl implements JobService { private final Scheduler scheduler; private final JobMapper jobMapper; @Autowired public JobServiceImpl(Scheduler scheduler, JobMapper jobMapper) { this.scheduler = scheduler; this.jobMapper = jobMapper; } /** * 添加并启动定时任务 * * @param form 表单参数 {@link JobForm} * @return {@link JobDetail} * @throws Exception 异常 */ @Override public void addJob(JobForm form) throws Exception { // 启动调度器 scheduler.start(); // 构建Job信息 JobDetail jobDetail = JobBuilder.newJob(JobUtil.getClass(form.getJobClassName()).getClass()).withIdentity(form.getJobClassName(), form.getJobGroupName()).build(); // Cron表达式调度构建器(即任务执行的时间) CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule(form.getCronExpression()); //根据Cron表达式构建一个Trigger CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(form.getJobClassName(), form.getJobGroupName()).withSchedule(cron).build(); try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { log.error("【定时任务】创建失败!", e); throw new Exception("【定时任务】创建失败!"); } } /** * 删除定时任务 * * @param form 表单参数 {@link JobForm} * @throws SchedulerException 异常 */ @Override public void deleteJob(JobForm form) throws SchedulerException { scheduler.pauseTrigger(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName())); scheduler.unscheduleJob(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName())); scheduler.deleteJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName())); } /** * 暂停定时任务 * * @param form 表单参数 {@link JobForm} * @throws SchedulerException 异常 */ @Override public void pauseJob(JobForm form) throws SchedulerException { scheduler.pauseJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName())); } /** * 恢复定时任务 * * @param form 表单参数 {@link JobForm} * @throws SchedulerException 异常 */ @Override public void resumeJob(JobForm form) throws SchedulerException { scheduler.resumeJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName())); } /** * 重新配置定时任务 * * @param form 表单参数 {@link JobForm} * @throws Exception 异常 */ @Override public void cronJob(JobForm form) throws Exception { try { TriggerKey triggerKey = TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()); // 表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(form.getCronExpression()); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); // 根据Cron表达式构建一个Trigger trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); // 按新的trigger重新设置job执行 scheduler.rescheduleJob(triggerKey, trigger); } catch (SchedulerException e) { log.error("【定时任务】更新失败!", e); throw new Exception("【定时任务】创建失败!"); } } /** * 查询定时任务列表 * * @param currentPage 当前页 * @param pageSize 每页条数 * @return 定时任务列表 */ @Override public PageInfo list(Integer currentPage, Integer pageSize) { PageHelper.startPage(currentPage, pageSize); List list = jobMapper.list(); return new PageInfo<>(list); } } ================================================ FILE: demo-task-quartz/src/main/java/com/xkcoding/task/quartz/util/JobUtil.java ================================================ package com.xkcoding.task.quartz.util; import com.xkcoding.task.quartz.job.base.BaseJob; /** *

    * 定时任务反射工具类 *

    * * @author yangkai.shen * @date Created in 2018-11-26 13:33 */ public class JobUtil { /** * 根据全类名获取Job实例 * * @param classname Job全类名 * @return {@link BaseJob} 实例 * @throws Exception 泛型获取异常 */ public static BaseJob getClass(String classname) throws Exception { Class clazz = Class.forName(classname); return (BaseJob) clazz.newInstance(); } } ================================================ FILE: demo-task-quartz/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&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: SpringBootDemoHikariCP max-lifetime: 60000 connection-timeout: 30000 quartz: # 参见 org.springframework.boot.autoconfigure.quartz.QuartzProperties job-store-type: jdbc wait-for-jobs-to-complete-on-shutdown: true scheduler-name: SpringBootDemoScheduler properties: org.quartz.threadPool.threadCount: 5 org.quartz.threadPool.threadPriority: 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true org.quartz.jobStore.misfireThreshold: 5000 org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 在调度流程的第一步,也就是拉取待即将触发的triggers时,是上锁的状态,即不会同时存在多个线程拉取到相同的trigger的情况,也就避免的重复调度的危险。参考:https://segmentfault.com/a/1190000015492260 org.quartz.jobStore.acquireTriggersWithinLock: true logging: level: com.xkcoding: debug com.xkcoding.task.quartz.mapper: trace mybatis: configuration: # 下划线转驼峰 map-underscore-to-camel-case: true mapper-locations: classpath:mappers/*.xml type-aliases-package: com.xkcoding.task.quartz.entity mapper: mappers: - tk.mybatis.mapper.common.Mapper not-empty: true style: camelhump wrap-keyword: "`{0}`" safe-delete: true safe-update: true identity: MYSQL pagehelper: auto-dialect: true helper-dialect: mysql reasonable: true params: count=countSql ================================================ FILE: demo-task-quartz/src/main/resources/mappers/JobMapper.xml ================================================ ================================================ FILE: demo-task-quartz/src/main/resources/static/job.html ================================================ spring-boot-demo-task-quartz
    查询 添加

    © Quartz 定时任务管理

    ================================================ FILE: demo-task-quartz/src/test/java/com/xkcoding/task/quartz/SpringBootDemoTaskQuartzApplicationTests.java ================================================ package com.xkcoding.task.quartz; 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 SpringBootDemoTaskQuartzApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-task-xxl-job/.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-task-xxl-job/README.md ================================================ # spring-boot-demo-task-xxl-job > 此 demo 主要演示了 Spring Boot 如何集成 XXL-JOB 实现分布式定时任务,并提供绕过 `xxl-job-admin` 对定时任务的管理的方法,包括定时任务列表,触发器列表,新增定时任务,删除定时任务,停止定时任务,启动定时任务,修改定时任务,手动触发定时任务。 ## 1. xxl-job-admin调度中心 > https://github.com/xuxueli/xxl-job.git 克隆 调度中心代码 ```bash $ git clone https://github.com/xuxueli/xxl-job.git ``` ### 1.1. 创建调度中心的表结构 数据库脚本地址:`/xxl-job/doc/db/tables_xxl_job.sql` ### 1.2. 修改 application.properties ```properties server.port=18080 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&useSSL=false spring.datasource.username=root spring.datasource.password=root ``` ### 1.3. 修改日志配置文件 logback.xml ```xml ``` ### 1.4. 启动调度中心 Run `XxlJobAdminApplication` 默认用户名密码:admin/admin ![image-20190808105554414](https://static.xkcoding.com/spring-boot-demo/2019-08-08-025555.png) ![image-20190808105628852](https://static.xkcoding.com/spring-boot-demo/2019-08-08-025629.png) ## 2. 编写执行器项目 ### 2.1. pom.xml ```xml 4.0.0 spring-boot-demo-task-xxl-job 1.0.0-SNAPSHOT jar spring-boot-demo-task-xxl-job Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.1.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true com.xuxueli xxl-job-core ${xxl-job.version} org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true spring-boot-demo-task-xxl-job org.springframework.boot spring-boot-maven-plugin ``` ### 2.2. 编写 配置类 XxlJobProps.java ```java /** *

    * xxl-job 配置 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:25 */ @Data @ConfigurationProperties(prefix = "xxl.job") public class XxlJobProps { /** * 调度中心配置 */ private XxlJobAdminProps admin; /** * 执行器配置 */ private XxlJobExecutorProps executor; /** * 与调度中心交互的accessToken */ private String accessToken; @Data public static class XxlJobAdminProps { /** * 调度中心地址 */ private String address; } @Data public static class XxlJobExecutorProps { /** * 执行器名称 */ private String appName; /** * 执行器 IP */ private String ip; /** * 执行器端口 */ private int port; /** * 执行器日志 */ private String logPath; /** * 执行器日志保留天数,-1 */ private int logRetentionDays; } } ``` ### 2.3. 编写配置文件 application.yml ```yaml server: port: 8080 servlet: context-path: /demo xxl: job: # 执行器通讯TOKEN [选填]:非空时启用; access-token: admin: # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; address: http://localhost:18080/xxl-job-admin executor: # 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 app-name: spring-boot-demo-task-xxl-job-executor # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; ip: # 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; port: 9999 # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; log-path: logs/spring-boot-demo-task-xxl-job/task-log # 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效; log-retention-days: -1 ``` ### 2.4. 编写自动装配类 XxlConfig.java ```java /** *

    * xxl-job 自动装配 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:20 */ @Slf4j @Configuration @EnableConfigurationProperties(XxlJobProps.class) @RequiredArgsConstructor(onConstructor_ = @Autowired) public class XxlJobConfig { private final XxlJobProps xxlJobProps; @Bean(initMethod = "start", destroyMethod = "destroy") public XxlJobSpringExecutor xxlJobExecutor() { log.info(">>>>>>>>>>> xxl-job config init."); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(xxlJobProps.getAdmin().getAddress()); xxlJobSpringExecutor.setAccessToken(xxlJobProps.getAccessToken()); xxlJobSpringExecutor.setAppName(xxlJobProps.getExecutor().getAppName()); xxlJobSpringExecutor.setIp(xxlJobProps.getExecutor().getIp()); xxlJobSpringExecutor.setPort(xxlJobProps.getExecutor().getPort()); xxlJobSpringExecutor.setLogPath(xxlJobProps.getExecutor().getLogPath()); xxlJobSpringExecutor.setLogRetentionDays(xxlJobProps.getExecutor().getLogRetentionDays()); return xxlJobSpringExecutor; } } ``` ### 2.5. 编写具体的定时逻辑 DemoTask.java ```java /** *

    * 测试定时任务 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:15 */ @Slf4j @Component @JobHandler("demoTask") public class DemoTask extends IJobHandler { /** * execute handler, invoked when executor receives a scheduling request * * @param param 定时任务参数 * @return 执行状态 * @throws Exception 任务异常 */ @Override public ReturnT execute(String param) throws Exception { // 可以动态获取传递过来的参数,根据参数不同,当前调度的任务不同 log.info("【param】= {}", param); XxlJobLogger.log("demo task run at : {}", DateUtil.now()); return RandomUtil.randomInt(1, 11) % 2 == 0 ? SUCCESS : FAIL; } } ``` ### 2.6. 启动执行器 Run `SpringBootDemoTaskXxlJobApplication` ## 3. 配置定时任务 ### 3.1. 将启动的执行器添加到调度中心 执行器管理 - 新增执行器 ![image-20190808105910203](https://static.xkcoding.com/spring-boot-demo/2019-08-08-025910.png) ### 3.2. 添加定时任务 任务管理 - 新增 - 保存 ![image-20190808110127956](https://static.xkcoding.com/spring-boot-demo/2019-08-08-030128.png) ### 3.3. 启停定时任务 任务列表的操作列,拥有以下操作:执行、启动/停止、日志、编辑、删除 执行:单次触发任务,不影响定时逻辑 启动:启动定时任务 停止:停止定时任务 日志:查看当前任务执行日志 编辑:更新定时任务 删除:删除定时任务 ## 4. 使用API添加定时任务 > 实际场景中,如果添加定时任务都需要手动在 xxl-job-admin 去操作,这样可能比较麻烦,用户更希望在自己的页面,添加定时任务参数、定时调度表达式,然后通过 API 的方式添加定时任务 ### 4.1. 改造xxl-job-admin #### 4.1.1. 修改 JobGroupController.java ```java ... // 添加执行器列表 @RequestMapping("/list") @ResponseBody // 去除权限校验 @PermissionLimit(limit = false) public ReturnT> list(){ return new ReturnT<>(xxlJobGroupDao.findAll()); } ... ``` #### 4.1.2. 修改 JobInfoController.java ```java // 分别在 pageList、add、update、remove、pause、start、triggerJob 方法上添加注解,去除权限校验 @PermissionLimit(limit = false) ``` ### 4.2. 改造 执行器项目 #### 4.2.1. 添加手动触发类 ```java /** *

    * 手动操作 xxl-job *

    * * @author yangkai.shen * @date Created in 2019-08-07 14:58 */ @Slf4j @RestController @RequestMapping("/xxl-job") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class ManualOperateController { private final static String baseUri = "http://127.0.0.1:18080/xxl-job-admin"; private final static String JOB_INFO_URI = "/jobinfo"; private final static String JOB_GROUP_URI = "/jobgroup"; /** * 任务组列表,xxl-job叫做触发器列表 */ @GetMapping("/group") public String xxlJobGroup() { HttpResponse execute = HttpUtil.createGet(baseUri + JOB_GROUP_URI + "/list").execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 分页任务列表 * * @param page 当前页,第一页 -> 0 * @param size 每页条数,默认10 * @return 分页任务列表 */ @GetMapping("/list") public String xxlJobList(Integer page, Integer size) { Map jobInfo = Maps.newHashMap(); jobInfo.put("start", page != null ? page : 0); jobInfo.put("length", size != null ? size : 10); jobInfo.put("jobGroup", 2); jobInfo.put("triggerStatus", -1); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/pageList").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动保存任务 */ @GetMapping("/add") public String xxlJobAdd() { Map jobInfo = Maps.newHashMap(); jobInfo.put("jobGroup", 2); jobInfo.put("jobCron", "0 0/1 * * * ? *"); jobInfo.put("jobDesc", "手动添加的任务"); jobInfo.put("author", "admin"); jobInfo.put("executorRouteStrategy", "ROUND"); jobInfo.put("executorHandler", "demoTask"); jobInfo.put("executorParam", "手动添加的任务的参数"); jobInfo.put("executorBlockStrategy", ExecutorBlockStrategyEnum.SERIAL_EXECUTION); jobInfo.put("glueType", GlueTypeEnum.BEAN); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/add").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动触发一次任务 */ @GetMapping("/trigger") public String xxlJobTrigger() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); jobInfo.put("executorParam", JSONUtil.toJsonStr(jobInfo)); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/trigger").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动删除任务 */ @GetMapping("/remove") public String xxlJobRemove() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/remove").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动停止任务 */ @GetMapping("/stop") public String xxlJobStop() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/stop").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动启动任务 */ @GetMapping("/start") public String xxlJobStart() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/start").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } } ``` > 后端其余代码请 clone 本项目,查看具体代码 ## 参考 - [《分布式任务调度平台xxl-job》](http://www.xuxueli.com/xxl-job/#/) ================================================ FILE: demo-task-xxl-job/pom.xml ================================================ 4.0.0 demo-task-xxl-job 1.0.0-SNAPSHOT jar demo-task-xxl-job Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 2.1.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true com.xuxueli xxl-job-core ${xxl-job.version} org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-task-xxl-job org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-task-xxl-job/src/main/java/com/xkcoding/task/xxl/job/SpringBootDemoTaskXxlJobApplication.java ================================================ package com.xkcoding.task.xxl.job; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:13 */ @SpringBootApplication public class SpringBootDemoTaskXxlJobApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTaskXxlJobApplication.class, args); } } ================================================ FILE: demo-task-xxl-job/src/main/java/com/xkcoding/task/xxl/job/config/XxlJobConfig.java ================================================ package com.xkcoding.task.xxl.job.config; import com.xkcoding.task.xxl.job.config.props.XxlJobProps; import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** *

    * xxl-job 自动装配 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:20 */ @Slf4j @Configuration @EnableConfigurationProperties(XxlJobProps.class) @RequiredArgsConstructor(onConstructor_ = @Autowired) public class XxlJobConfig { private final XxlJobProps xxlJobProps; @Bean(initMethod = "start", destroyMethod = "destroy") public XxlJobSpringExecutor xxlJobExecutor() { log.info(">>>>>>>>>>> xxl-job config init."); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(xxlJobProps.getAdmin().getAddress()); xxlJobSpringExecutor.setAccessToken(xxlJobProps.getAccessToken()); xxlJobSpringExecutor.setAppName(xxlJobProps.getExecutor().getAppName()); xxlJobSpringExecutor.setIp(xxlJobProps.getExecutor().getIp()); xxlJobSpringExecutor.setPort(xxlJobProps.getExecutor().getPort()); xxlJobSpringExecutor.setLogPath(xxlJobProps.getExecutor().getLogPath()); xxlJobSpringExecutor.setLogRetentionDays(xxlJobProps.getExecutor().getLogRetentionDays()); return xxlJobSpringExecutor; } } ================================================ FILE: demo-task-xxl-job/src/main/java/com/xkcoding/task/xxl/job/config/props/XxlJobProps.java ================================================ package com.xkcoding.task.xxl.job.config.props; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** *

    * xxl-job 配置 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:25 */ @Data @ConfigurationProperties(prefix = "xxl.job") public class XxlJobProps { /** * 调度中心配置 */ private XxlJobAdminProps admin; /** * 执行器配置 */ private XxlJobExecutorProps executor; /** * 与调度中心交互的accessToken */ private String accessToken; @Data public static class XxlJobAdminProps { /** * 调度中心地址 */ private String address; } @Data public static class XxlJobExecutorProps { /** * 执行器名称 */ private String appName; /** * 执行器 IP */ private String ip; /** * 执行器端口 */ private int port; /** * 执行器日志 */ private String logPath; /** * 执行器日志保留天数,-1 */ private int logRetentionDays; } } ================================================ FILE: demo-task-xxl-job/src/main/java/com/xkcoding/task/xxl/job/controller/ManualOperateController.java ================================================ package com.xkcoding.task.xxl.job.controller; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONUtil; import com.google.common.collect.Maps; import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; import com.xxl.job.core.glue.GlueTypeEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; /** *

    * 手动操作 xxl-job *

    * * @author yangkai.shen * @date Created in 2019-08-07 14:58 */ @Slf4j @RestController @RequestMapping("/xxl-job") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class ManualOperateController { private final static String baseUri = "http://127.0.0.1:18080/xxl-job-admin"; private final static String JOB_INFO_URI = "/jobinfo"; private final static String JOB_GROUP_URI = "/jobgroup"; /** * 任务组列表,xxl-job叫做触发器列表 */ @GetMapping("/group") public String xxlJobGroup() { HttpResponse execute = HttpUtil.createGet(baseUri + JOB_GROUP_URI + "/list").execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 分页任务列表 * * @param page 当前页,第一页 -> 0 * @param size 每页条数,默认10 * @return 分页任务列表 */ @GetMapping("/list") public String xxlJobList(Integer page, Integer size) { Map jobInfo = Maps.newHashMap(); jobInfo.put("start", page != null ? page : 0); jobInfo.put("length", size != null ? size : 10); jobInfo.put("jobGroup", 2); jobInfo.put("triggerStatus", -1); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/pageList").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动保存任务 */ @GetMapping("/add") public String xxlJobAdd() { Map jobInfo = Maps.newHashMap(); jobInfo.put("jobGroup", 2); jobInfo.put("jobCron", "0 0/1 * * * ? *"); jobInfo.put("jobDesc", "手动添加的任务"); jobInfo.put("author", "admin"); jobInfo.put("executorRouteStrategy", "ROUND"); jobInfo.put("executorHandler", "demoTask"); jobInfo.put("executorParam", "手动添加的任务的参数"); jobInfo.put("executorBlockStrategy", ExecutorBlockStrategyEnum.SERIAL_EXECUTION); jobInfo.put("glueType", GlueTypeEnum.BEAN); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/add").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动触发一次任务 */ @GetMapping("/trigger") public String xxlJobTrigger() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); jobInfo.put("executorParam", JSONUtil.toJsonStr(jobInfo)); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/trigger").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动删除任务 */ @GetMapping("/remove") public String xxlJobRemove() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/remove").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动停止任务 */ @GetMapping("/stop") public String xxlJobStop() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/stop").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } /** * 测试手动启动任务 */ @GetMapping("/start") public String xxlJobStart() { Map jobInfo = Maps.newHashMap(); jobInfo.put("id", 4); HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/start").form(jobInfo).execute(); log.info("【execute】= {}", execute); return execute.body(); } } ================================================ FILE: demo-task-xxl-job/src/main/java/com/xkcoding/task/xxl/job/task/DemoTask.java ================================================ package com.xkcoding.task.xxl.job.task; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.RandomUtil; import com.xxl.job.core.biz.model.ReturnT; import com.xxl.job.core.handler.IJobHandler; import com.xxl.job.core.handler.annotation.JobHandler; import com.xxl.job.core.log.XxlJobLogger; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** *

    * 测试定时任务 *

    * * @author yangkai.shen * @date Created in 2019-08-07 10:15 */ @Slf4j @Component @JobHandler("demoTask") public class DemoTask extends IJobHandler { /** * execute handler, invoked when executor receives a scheduling request * * @param param 定时任务参数 * @return 执行状态 * @throws Exception 任务异常 */ @Override public ReturnT execute(String param) throws Exception { // 可以动态获取传递过来的参数,根据参数不同,当前调度的任务不同 log.info("【param】= {}", param); XxlJobLogger.log("demo task run at : {}", DateUtil.now()); return RandomUtil.randomInt(1, 11) % 2 == 0 ? SUCCESS : FAIL; } } ================================================ FILE: demo-task-xxl-job/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo xxl: job: # 执行器通讯TOKEN [选填]:非空时启用; access-token: admin: # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; address: http://localhost:18080/xxl-job-admin executor: # 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 app-name: spring-boot-demo-task-xxl-job-executor # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; ip: # 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; port: 9999 # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; log-path: logs/spring-boot-demo-task-xxl-job/task-log # 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效; log-retention-days: -1 ================================================ FILE: demo-template-beetl/.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-template-beetl/README.md ================================================ # spring-boot-demo-template-beetl > 本 demo 主要演示了 Spring Boot 项目如何集成 beetl 模板引擎 ## pom.xml ```xml 4.0.0 spring-boot-demo-template-beetl 1.0.0-SNAPSHOT jar spring-boot-demo-template-beetl Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.1.63.RELEASE com.ibeetl beetl-framework-starter ${ibeetl.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all spring-boot-demo-template-beetl org.springframework.boot spring-boot-maven-plugin ``` ## IndexController.java ```java /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-10 11:17 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index.btl"); mv.addObject(user); } return mv; } } ``` ## UserController.java ```java /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-10 11:17 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login.btl"); } } ``` ## index.html ```jsp <% include("/common/head.html"){} %>
    欢迎登录,${user.name}!
    ``` ## login.html ```jsp <% include("/common/head.html"){} %>
    用户名 密码
    ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo ``` ## Beetl 语法糖学习文档 http://ibeetl.com/guide/#beetl ================================================ FILE: demo-template-beetl/pom.xml ================================================ 4.0.0 demo-template-beetl 1.0.0-SNAPSHOT jar demo-template-beetl Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.1.63.RELEASE com.ibeetl beetl-framework-starter ${ibeetl.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all demo-template-beetl org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-template-beetl/src/main/java/com/xkcoding/template/beetl/SpringBootDemoTemplateBeetlApplication.java ================================================ package com.xkcoding.template.beetl; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-10 11:17 */ @SpringBootApplication public class SpringBootDemoTemplateBeetlApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTemplateBeetlApplication.class, args); } } ================================================ FILE: demo-template-beetl/src/main/java/com/xkcoding/template/beetl/controller/IndexController.java ================================================ package com.xkcoding.template.beetl.controller; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.template.beetl.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-10 11:17 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index.btl"); mv.addObject(user); } return mv; } } ================================================ FILE: demo-template-beetl/src/main/java/com/xkcoding/template/beetl/controller/UserController.java ================================================ package com.xkcoding.template.beetl.controller; import com.xkcoding.template.beetl.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-10 11:17 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login.btl"); } } ================================================ FILE: demo-template-beetl/src/main/java/com/xkcoding/template/beetl/model/User.java ================================================ package com.xkcoding.template.beetl.model; import lombok.Data; /** *

    * 用户 model *

    * * @author yangkai.shen * @date Created in 2018-10-10 11:18 */ @Data public class User { private String name; private String password; } ================================================ FILE: demo-template-beetl/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ================================================ FILE: demo-template-beetl/src/main/resources/templates/common/head.html ================================================ spring-boot-demo-template-beetl ================================================ FILE: demo-template-beetl/src/main/resources/templates/page/index.btl ================================================ <% include("/common/head.html"){} %>
    欢迎登录,${user.name}!
    ================================================ FILE: demo-template-beetl/src/main/resources/templates/page/login.btl ================================================ <% include("/common/head.html"){} %>
    用户名 密码
    ================================================ FILE: demo-template-beetl/src/test/java/com/xkcoding/template/beetl/SpringBootDemoTemplateBeetlApplicationTests.java ================================================ package com.xkcoding.template.beetl; 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 SpringBootDemoTemplateBeetlApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-template-enjoy/.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-template-enjoy/README.md ================================================ # spring-boot-demo-template-enjoy > 本 demo 主要演示了 Spring Boot 项目如何集成 enjoy 模板引擎。 ## pom.xml ```xml 4.0.0 spring-boot-demo-template-enjoy 1.0.0-SNAPSHOT jar spring-boot-demo-template-enjoy Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 3.5 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.jfinal enjoy ${enjoy.version} org.projectlombok lombok true cn.hutool hutool-all spring-boot-demo-template-enjoy org.springframework.boot spring-boot-maven-plugin ``` ## EnjoyConfig.java ```java /** *

    * Enjoy 模板配置类 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:06 */ @Configuration public class EnjoyConfig { @Bean(name = "jfinalViewResolver") public JFinalViewResolver getJFinalViewResolver() { JFinalViewResolver jfr = new JFinalViewResolver(); // setDevMode 配置放在最前面 jfr.setDevMode(true); // 使用 ClassPathSourceFactory 从 class path 与 jar 包中加载模板文件 jfr.setSourceFactory(new ClassPathSourceFactory()); // 在使用 ClassPathSourceFactory 时要使用 setBaseTemplatePath // 代替 jfr.setPrefix("/view/") JFinalViewResolver.engine.setBaseTemplatePath("/templates/"); jfr.setSessionInView(true); jfr.setSuffix(".html"); jfr.setContentType("text/html;charset=UTF-8"); jfr.setOrder(0); return jfr; } } ``` ## IndexController.java ```java /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:22 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index"); mv.addObject(user); } return mv; } } ``` ## UserController.java ```java /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:24 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login"); } } ``` ## index.html ```jsp #include("/common/head.html")
    欢迎登录,#(user.name)!
    ``` ## login.html ```jsp #include("/common/head.html")
    用户名 密码
    ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo ``` ## Enjoy 语法糖学习文档 http://www.jfinal.com/doc/6-1 ================================================ FILE: demo-template-enjoy/pom.xml ================================================ 4.0.0 demo-template-enjoy 1.0.0-SNAPSHOT jar demo-template-enjoy Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 3.5 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.jfinal enjoy ${enjoy.version} org.projectlombok lombok true cn.hutool hutool-all demo-template-enjoy org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-template-enjoy/src/main/java/com/xkcoding/template/enjoy/SpringBootDemoTemplateEnjoyApplication.java ================================================ package com.xkcoding.template.enjoy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:06 */ @SpringBootApplication public class SpringBootDemoTemplateEnjoyApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTemplateEnjoyApplication.class, args); } } ================================================ FILE: demo-template-enjoy/src/main/java/com/xkcoding/template/enjoy/config/EnjoyConfig.java ================================================ package com.xkcoding.template.enjoy.config; import com.jfinal.template.ext.spring.JFinalViewResolver; import com.jfinal.template.source.ClassPathSourceFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** *

    * Enjoy 模板配置类 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:06 */ @Configuration public class EnjoyConfig { @Bean(name = "jfinalViewResolver") public JFinalViewResolver getJFinalViewResolver() { JFinalViewResolver jfr = new JFinalViewResolver(); // setDevMode 配置放在最前面 jfr.setDevMode(true); // 使用 ClassPathSourceFactory 从 class path 与 jar 包中加载模板文件 jfr.setSourceFactory(new ClassPathSourceFactory()); // 在使用 ClassPathSourceFactory 时要使用 setBaseTemplatePath // 代替 jfr.setPrefix("/view/") JFinalViewResolver.engine.setBaseTemplatePath("/templates/"); jfr.setSessionInView(true); jfr.setSuffix(".html"); jfr.setContentType("text/html;charset=UTF-8"); jfr.setOrder(0); return jfr; } } ================================================ FILE: demo-template-enjoy/src/main/java/com/xkcoding/template/enjoy/controller/IndexController.java ================================================ package com.xkcoding.template.enjoy.controller; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.template.enjoy.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:22 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index"); mv.addObject(user); } return mv; } } ================================================ FILE: demo-template-enjoy/src/main/java/com/xkcoding/template/enjoy/controller/UserController.java ================================================ package com.xkcoding.template.enjoy.controller; import com.xkcoding.template.enjoy.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:24 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login"); } } ================================================ FILE: demo-template-enjoy/src/main/java/com/xkcoding/template/enjoy/model/User.java ================================================ package com.xkcoding.template.enjoy.model; import lombok.Data; /** *

    * 用户 model *

    * * @author yangkai.shen * @date Created in 2018-10-11 14:21 */ @Data public class User { private String name; private String password; } ================================================ FILE: demo-template-enjoy/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ================================================ FILE: demo-template-enjoy/src/main/resources/templates/common/head.html ================================================ spring-boot-demo-template-enjoy ================================================ FILE: demo-template-enjoy/src/main/resources/templates/page/index.html ================================================ #include("/common/head.html")
    欢迎登录,#(user.name)!
    ================================================ FILE: demo-template-enjoy/src/main/resources/templates/page/login.html ================================================ #include("/common/head.html")
    用户名 密码
    ================================================ FILE: demo-template-enjoy/src/test/java/com/xkcoding/template/enjoy/SpringBootDemoTemplateEnjoyApplicationTests.java ================================================ package com.xkcoding.template.enjoy; 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 SpringBootDemoTemplateEnjoyApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-template-freemarker/.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-template-freemarker/README.md ================================================ # spring-boot-demo-template-freemarker > 本 demo 主要演示了 Spring Boot 项目如何集成 freemarker 模板引擎 ## pom.xml ```xml 4.0.0 spring-boot-demo-template-freemarker 1.0.0-SNAPSHOT jar spring-boot-demo-template-freemarker 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-freemarker org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all spring-boot-demo-template-freemarker org.springframework.boot spring-boot-maven-plugin ``` ## IndexController.java ```java /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-019 15:07 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("index"); mv.addObject(user); } return mv; } } ``` ## UserController.java ```java /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-019 15:11 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("login"); } } ``` ## index.ftl ```jsp <#include "./common/head.ftl">
    欢迎登录,${user.name}!
    ``` ## login.ftl ```jsp <#include "./common/head.ftl">
    用户名 密码
    ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo spring: freemarker: suffix: .ftl cache: false charset: UTF-8 ``` ## Freemarker 语法糖学习文档 https://freemarker.apache.org/docs/dgui.html ================================================ FILE: demo-template-freemarker/pom.xml ================================================ 4.0.0 demo-template-freemarker 1.0.0-SNAPSHOT jar demo-template-freemarker 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-freemarker org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true cn.hutool hutool-all demo-template-freemarker org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-template-freemarker/src/main/java/com/xkcoding/template/freemarker/SpringBootDemoTemplateFreemarkerApplication.java ================================================ package com.xkcoding.template.freemarker; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-19 15:17 */ @SpringBootApplication public class SpringBootDemoTemplateFreemarkerApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTemplateFreemarkerApplication.class, args); } } ================================================ FILE: demo-template-freemarker/src/main/java/com/xkcoding/template/freemarker/controller/IndexController.java ================================================ package com.xkcoding.template.freemarker.controller; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.template.freemarker.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-19 15:07 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index"); mv.addObject(user); } return mv; } } ================================================ FILE: demo-template-freemarker/src/main/java/com/xkcoding/template/freemarker/controller/UserController.java ================================================ package com.xkcoding.template.freemarker.controller; import com.xkcoding.template.freemarker.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-19 15:11 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login"); } } ================================================ FILE: demo-template-freemarker/src/main/java/com/xkcoding/template/freemarker/model/User.java ================================================ package com.xkcoding.template.freemarker.model; import lombok.Data; /** *

    * 用户 model *

    * * @author yangkai.shen * @date Created in 2018-10-19 15:06 */ @Data public class User { private String name; private String password; } ================================================ FILE: demo-template-freemarker/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: freemarker: suffix: .ftl cache: false charset: UTF-8 ================================================ FILE: demo-template-freemarker/src/main/resources/templates/common/head.ftl ================================================ spring-boot-template-freemarker ================================================ FILE: demo-template-freemarker/src/main/resources/templates/page/index.ftl ================================================ <#include "../common/head.ftl">
    欢迎登录,${user.name}!
    ================================================ FILE: demo-template-freemarker/src/main/resources/templates/page/login.ftl ================================================ <#include "../common/head.ftl">
    用户名 密码
    ================================================ FILE: demo-template-freemarker/src/test/java/com/xkcoding/template/freemarker/SpringBootDemoTemplateFreemarkerApplicationTests.java ================================================ package com.xkcoding.template.freemarker; 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 SpringBootDemoTemplateFreemarkerApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-template-thymeleaf/.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-template-thymeleaf/README.md ================================================ # spring-boot-demo-template-thymeleaf > 本 demo 主要演示了 Spring Boot 项目如何集成 thymeleaf 模板引擎 ## pom.xml ```xml 4.0.0 spring-boot-demo-template-thymeleaf 1.0.0-SNAPSHOT jar spring-boot-demo-template-thymeleaf 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 cn.hutool hutool-all spring-boot-demo-template-thymeleaf org.springframework.boot spring-boot-maven-plugin ``` ## IndexController.java ```java /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-10 10:12 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index"); mv.addObject(user); } return mv; } } ``` ## UserController.java ```java /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-10 10:11 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login"); } } ``` ## index.html ```jsp
    欢迎登录,
    ``` ## login.html ```jsp
    用户名 密码
    ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo spring: thymeleaf: mode: HTML encoding: UTF-8 servlet: content-type: text/html cache: false ``` ## Thymeleaf语法糖学习文档 https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html ================================================ FILE: demo-template-thymeleaf/pom.xml ================================================ 4.0.0 demo-template-thymeleaf 1.0.0-SNAPSHOT jar demo-template-thymeleaf 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 cn.hutool hutool-all demo-template-thymeleaf org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-template-thymeleaf/src/main/java/com/xkcoding/template/thymeleaf/SpringBootDemoTemplateThymeleafApplication.java ================================================ package com.xkcoding.template.thymeleaf; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-10 10:10 */ @SpringBootApplication public class SpringBootDemoTemplateThymeleafApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTemplateThymeleafApplication.class, args); } } ================================================ FILE: demo-template-thymeleaf/src/main/java/com/xkcoding/template/thymeleaf/controller/IndexController.java ================================================ package com.xkcoding.template.thymeleaf.controller; import cn.hutool.core.util.ObjectUtil; import com.xkcoding.template.thymeleaf.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 主页 *

    * * @author yangkai.shen * @date Created in 2018-10-10 10:12 */ @Controller @Slf4j public class IndexController { @GetMapping(value = {"", "/"}) public ModelAndView index(HttpServletRequest request) { ModelAndView mv = new ModelAndView(); User user = (User) request.getSession().getAttribute("user"); if (ObjectUtil.isNull(user)) { mv.setViewName("redirect:/user/login"); } else { mv.setViewName("page/index"); mv.addObject(user); } return mv; } } ================================================ FILE: demo-template-thymeleaf/src/main/java/com/xkcoding/template/thymeleaf/controller/UserController.java ================================================ package com.xkcoding.template.thymeleaf.controller; import com.xkcoding.template.thymeleaf.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** *

    * 用户页面 *

    * * @author yangkai.shen * @date Created in 2018-10-10 10:11 */ @Controller @RequestMapping("/user") @Slf4j public class UserController { @PostMapping("/login") public ModelAndView login(User user, HttpServletRequest request) { ModelAndView mv = new ModelAndView(); mv.addObject(user); mv.setViewName("redirect:/"); request.getSession().setAttribute("user", user); return mv; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("page/login"); } } ================================================ FILE: demo-template-thymeleaf/src/main/java/com/xkcoding/template/thymeleaf/model/User.java ================================================ package com.xkcoding.template.thymeleaf.model; import lombok.Data; /** *

    * 用户 model *

    * * @author yangkai.shen * @date Created in 2018-10-10 10:11 */ @Data public class User { private String name; private String password; } ================================================ FILE: demo-template-thymeleaf/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo spring: thymeleaf: mode: HTML encoding: UTF-8 servlet: content-type: text/html cache: false ================================================ FILE: demo-template-thymeleaf/src/main/resources/templates/common/head.html ================================================ spring-boot-demo-template-thymeleaf ================================================ FILE: demo-template-thymeleaf/src/main/resources/templates/page/index.html ================================================
    欢迎登录,
    ================================================ FILE: demo-template-thymeleaf/src/main/resources/templates/page/login.html ================================================
    用户名 密码
    ================================================ FILE: demo-template-thymeleaf/src/test/java/com/xkcoding/template/thymeleaf/SpringBootDemoTemplateThymeleafApplicationTests.java ================================================ package com.xkcoding.template.thymeleaf; 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 SpringBootDemoTemplateThymeleafApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-tio/.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/ ================================================ FILE: demo-tio/README.md ================================================ # spring-boot-demo-tio ================================================ FILE: demo-tio/pom.xml ================================================ 4.0.0 com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT demo-tio 1.0.0-SNAPSHOT demo-tio Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-tio/src/main/java/com/xkcoding/springbootdemotio/SpringBootDemoTioApplication.java ================================================ package com.xkcoding.springbootdemotio; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-02-05 18:58 */ @SpringBootApplication public class SpringBootDemoTioApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoTioApplication.class, args); } } ================================================ FILE: demo-tio/src/main/resources/application.properties ================================================ ================================================ FILE: demo-tio/src/test/java/com/xkcoding/springbootdemotio/SpringBootDemoTioApplicationTests.java ================================================ package com.xkcoding.springbootdemotio; 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 SpringBootDemoTioApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-uflo/.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-uflo/pom.xml ================================================ 4.0.0 demo-uflo 1.0.0-SNAPSHOT jar demo-uflo 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-uflo org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-uflo/src/main/java/com/xkcoding/uflo/SpringBootDemoUfloApplication.java ================================================ package com.xkcoding.uflo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringBootDemoUfloApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoUfloApplication.class, args); } } ================================================ FILE: demo-uflo/src/main/resources/application.properties ================================================ ================================================ FILE: demo-uflo/src/test/java/com/xkcoding/uflo/SpringBootDemoUfloApplicationTests.java ================================================ package com.xkcoding.uflo; 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 SpringBootDemoUfloApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-upload/.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-upload/README.md ================================================ # spring-boot-demo-upload > 本 demo 演示了 Spring Boot 如何实现本地文件上传以及如何上传文件至七牛云平台。前端使用 vue 和 iview 实现上传页面。 ## pom.xml ```xml 4.0.0 spring-boot-demo-upload 1.0.0-SNAPSHOT jar spring-boot-demo-upload Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 org.projectlombok lombok true org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.qiniu qiniu-java-sdk [7.2.0, 7.2.99] spring-boot-demo-upload org.springframework.boot spring-boot-maven-plugin ``` ## UploadConfig.java ```java /** *

    * 上传配置 *

    * * @author yangkai.shen * @date Created in 2018-10-23 14:09 */ @Configuration @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class}) @ConditionalOnProperty(prefix = "spring.http.multipart", name = "enabled", matchIfMissing = true) @EnableConfigurationProperties(MultipartProperties.class) public class UploadConfig { @Value("${qiniu.accessKey}") private String accessKey; @Value("${qiniu.secretKey}") private String secretKey; private final MultipartProperties multipartProperties; @Autowired public UploadConfig(MultipartProperties multipartProperties) { this.multipartProperties = multipartProperties; } /** * 上传配置 */ @Bean @ConditionalOnMissingBean public MultipartConfigElement multipartConfigElement() { return this.multipartProperties.createMultipartConfig(); } /** * 注册解析器 */ @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) @ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; } /** * 华东机房 */ @Bean public com.qiniu.storage.Configuration qiniuConfig() { return new com.qiniu.storage.Configuration(Zone.zone0()); } /** * 构建一个七牛上传工具实例 */ @Bean public UploadManager uploadManager() { return new UploadManager(qiniuConfig()); } /** * 认证信息实例 */ @Bean public Auth auth() { return Auth.create(accessKey, secretKey); } /** * 构建七牛空间管理实例 */ @Bean public BucketManager bucketManager() { return new BucketManager(auth(), qiniuConfig()); } } ``` ## UploadController.java ```java /** *

    * 文件上传 Controller *

    * * @author yangkai.shen * @date Created in 2018-11-06 16:33 */ @RestController @Slf4j @RequestMapping("/upload") public class UploadController { @Value("${spring.servlet.multipart.location}") private String fileTempPath; @Value("${qiniu.prefix}") private String prefix; private final IQiNiuService qiNiuService; @Autowired public UploadController(IQiNiuService qiNiuService) { this.qiNiuService = qiNiuService; } @PostMapping(value = "/local", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Dict local(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Dict.create().set("code", 400).set("message", "文件内容为空"); } String fileName = file.getOriginalFilename(); String rawFileName = StrUtil.subBefore(fileName, ".", true); String fileType = StrUtil.subAfter(fileName, ".", true); String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType; try { file.transferTo(new File(localFilePath)); } catch (IOException e) { log.error("【文件上传至本地】失败,绝对路径:{}", localFilePath); return Dict.create().set("code", 500).set("message", "文件上传失败"); } log.info("【文件上传至本地】绝对路径:{}", localFilePath); return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", localFilePath)); } @PostMapping(value = "/yun", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Dict yun(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Dict.create().set("code", 400).set("message", "文件内容为空"); } String fileName = file.getOriginalFilename(); String rawFileName = StrUtil.subBefore(fileName, ".", true); String fileType = StrUtil.subAfter(fileName, ".", true); String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType; try { file.transferTo(new File(localFilePath)); Response response = qiNiuService.uploadFile(new File(localFilePath)); if (response.isOK()) { JSONObject jsonObject = JSONUtil.parseObj(response.bodyString()); String yunFileName = jsonObject.getStr("key"); String yunFilePath = StrUtil.appendIfMissing(prefix, "/") + yunFileName; FileUtil.del(new File(localFilePath)); log.info("【文件上传至七牛云】绝对路径:{}", yunFilePath); return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", yunFileName).set("filePath", yunFilePath)); } else { log.error("【文件上传至七牛云】失败,{}", JSONUtil.toJsonStr(response)); FileUtil.del(new File(localFilePath)); return Dict.create().set("code", 500).set("message", "文件上传失败"); } } catch (IOException e) { log.error("【文件上传至七牛云】失败,绝对路径:{}", localFilePath); return Dict.create().set("code", 500).set("message", "文件上传失败"); } } } ``` ## QiNiuServiceImpl.java ```java /** *

    * 七牛云上传Service *

    * * @author yangkai.shen * @date Created in 2018-11-06 17:22 */ @Service @Slf4j public class QiNiuServiceImpl implements IQiNiuService, InitializingBean { private final UploadManager uploadManager; private final Auth auth; @Value("${qiniu.bucket}") private String bucket; private StringMap putPolicy; @Autowired public QiNiuServiceImpl(UploadManager uploadManager, Auth auth) { this.uploadManager = uploadManager; this.auth = auth; } /** * 七牛云上传文件 * * @param file 文件 * @return 七牛上传Response * @throws QiniuException 七牛异常 */ @Override public Response uploadFile(File file) throws QiniuException { Response response = this.uploadManager.put(file, file.getName(), getUploadToken()); int retry = 0; while (response.needRetry() && retry < 3) { response = this.uploadManager.put(file, file.getName(), getUploadToken()); retry++; } return response; } @Override public void afterPropertiesSet() { this.putPolicy = new StringMap(); putPolicy.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"width\":$(imageInfo.width), \"height\":${imageInfo.height}}"); } /** * 获取上传凭证 * * @return 上传凭证 */ private String getUploadToken() { return this.auth.uploadToken(bucket, null, 3600, putPolicy); } } ``` ## index.html ```html spring-boot-demo-upload

    本地上传

    选择文件 {{ local.loadingStatus ? '本地文件上传中' : '本地上传' }}
    状态:{{local.log.message}}
    文件名:{{local.log.fileName}}
    文件路径:{{local.log.filePath}}

    七牛云上传

    选择文件 {{ yun.loadingStatus ? '七牛云文件上传中' : '七牛云上传' }}
    状态:{{yun.log.message}}
    文件名:{{yun.log.fileName}}
    文件路径:{{yun.log.filePath}}
    ``` ## 参考 1. Spring 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#howto-multipart-file-upload-configuration 2. 七牛云官方文档:https://developer.qiniu.com/kodo/sdk/1239/java#5 ================================================ FILE: demo-upload/pom.xml ================================================ 4.0.0 demo-upload 1.0.0-SNAPSHOT jar demo-upload Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 org.projectlombok lombok true org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all com.qiniu qiniu-java-sdk [7.2.0, 7.2.99] demo-upload org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-upload/src/main/java/com/xkcoding/upload/SpringBootDemoUploadApplication.java ================================================ package com.xkcoding.upload; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动类 *

    * * @author yangkai.shen * @date Created in 2018-10-20 21:23 */ @SpringBootApplication public class SpringBootDemoUploadApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoUploadApplication.class, args); } } ================================================ FILE: demo-upload/src/main/java/com/xkcoding/upload/config/UploadConfig.java ================================================ package com.xkcoding.upload.config; import com.qiniu.common.Zone; import com.qiniu.storage.BucketManager; import com.qiniu.storage.UploadManager; import com.qiniu.util.Auth; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.StandardServletMultipartResolver; import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.MultipartConfigElement; import javax.servlet.Servlet; /** *

    * 上传配置 *

    * * @author yangkai.shen * @date Created in 2018-10-23 14:09 */ @Configuration @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class}) @ConditionalOnProperty(prefix = "spring.http.multipart", name = "enabled", matchIfMissing = true) @EnableConfigurationProperties(MultipartProperties.class) public class UploadConfig { @Value("${qiniu.accessKey}") private String accessKey; @Value("${qiniu.secretKey}") private String secretKey; private final MultipartProperties multipartProperties; @Autowired public UploadConfig(MultipartProperties multipartProperties) { this.multipartProperties = multipartProperties; } /** * 上传配置 */ @Bean @ConditionalOnMissingBean public MultipartConfigElement multipartConfigElement() { return this.multipartProperties.createMultipartConfig(); } /** * 注册解析器 */ @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) @ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; } /** * 华东机房 */ @Bean public com.qiniu.storage.Configuration qiniuConfig() { return new com.qiniu.storage.Configuration(Zone.zone0()); } /** * 构建一个七牛上传工具实例 */ @Bean public UploadManager uploadManager() { return new UploadManager(qiniuConfig()); } /** * 认证信息实例 */ @Bean public Auth auth() { return Auth.create(accessKey, secretKey); } /** * 构建七牛空间管理实例 */ @Bean public BucketManager bucketManager() { return new BucketManager(auth(), qiniuConfig()); } } ================================================ FILE: demo-upload/src/main/java/com/xkcoding/upload/controller/IndexController.java ================================================ package com.xkcoding.upload.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; /** *

    * 首页Controller *

    * * @author yangkai.shen * @date Created in 2018-10-20 21:22 */ @Controller public class IndexController { @GetMapping("") public String index() { return "index"; } } ================================================ FILE: demo-upload/src/main/java/com/xkcoding/upload/controller/UploadController.java ================================================ package com.xkcoding.upload.controller; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.qiniu.http.Response; import com.xkcoding.upload.service.IQiNiuService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; /** *

    * 文件上传 Controller *

    * * @author yangkai.shen * @date Created in 2018-11-06 16:33 */ @RestController @Slf4j @RequestMapping("/upload") public class UploadController { @Value("${spring.servlet.multipart.location}") private String fileTempPath; @Value("${qiniu.prefix}") private String prefix; private final IQiNiuService qiNiuService; @Autowired public UploadController(IQiNiuService qiNiuService) { this.qiNiuService = qiNiuService; } @PostMapping(value = "/local", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Dict local(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Dict.create().set("code", 400).set("message", "文件内容为空"); } String fileName = file.getOriginalFilename(); String rawFileName = StrUtil.subBefore(fileName, ".", true); String fileType = StrUtil.subAfter(fileName, ".", true); String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType; try { file.transferTo(new File(localFilePath)); } catch (IOException e) { log.error("【文件上传至本地】失败,绝对路径:{}", localFilePath); return Dict.create().set("code", 500).set("message", "文件上传失败"); } log.info("【文件上传至本地】绝对路径:{}", localFilePath); return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", localFilePath)); } @PostMapping(value = "/yun", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Dict yun(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Dict.create().set("code", 400).set("message", "文件内容为空"); } String fileName = file.getOriginalFilename(); String rawFileName = StrUtil.subBefore(fileName, ".", true); String fileType = StrUtil.subAfter(fileName, ".", true); String localFilePath = StrUtil.appendIfMissing(fileTempPath, "/") + rawFileName + "-" + DateUtil.current(false) + "." + fileType; try { file.transferTo(new File(localFilePath)); Response response = qiNiuService.uploadFile(new File(localFilePath)); if (response.isOK()) { JSONObject jsonObject = JSONUtil.parseObj(response.bodyString()); String yunFileName = jsonObject.getStr("key"); String yunFilePath = StrUtil.appendIfMissing(prefix, "/") + yunFileName; FileUtil.del(new File(localFilePath)); log.info("【文件上传至七牛云】绝对路径:{}", yunFilePath); return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", yunFileName).set("filePath", yunFilePath)); } else { log.error("【文件上传至七牛云】失败,{}", JSONUtil.toJsonStr(response)); FileUtil.del(new File(localFilePath)); return Dict.create().set("code", 500).set("message", "文件上传失败"); } } catch (IOException e) { log.error("【文件上传至七牛云】失败,绝对路径:{}", localFilePath); return Dict.create().set("code", 500).set("message", "文件上传失败"); } } } ================================================ FILE: demo-upload/src/main/java/com/xkcoding/upload/service/IQiNiuService.java ================================================ package com.xkcoding.upload.service; import com.qiniu.common.QiniuException; import com.qiniu.http.Response; import java.io.File; /** *

    * 七牛云上传Service *

    * * @author yangkai.shen * @date Created in 2018-11-06 17:21 */ public interface IQiNiuService { /** * 七牛云上传文件 * * @param file 文件 * @return 七牛上传Response * @throws QiniuException 七牛异常 */ Response uploadFile(File file) throws QiniuException; } ================================================ FILE: demo-upload/src/main/java/com/xkcoding/upload/service/impl/QiNiuServiceImpl.java ================================================ package com.xkcoding.upload.service.impl; import com.qiniu.common.QiniuException; import com.qiniu.http.Response; import com.qiniu.storage.UploadManager; import com.qiniu.util.Auth; import com.qiniu.util.StringMap; import com.xkcoding.upload.service.IQiNiuService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.File; /** *

    * 七牛云上传Service *

    * * @author yangkai.shen * @date Created in 2018-11-06 17:22 */ @Service @Slf4j public class QiNiuServiceImpl implements IQiNiuService, InitializingBean { private final UploadManager uploadManager; private final Auth auth; @Value("${qiniu.bucket}") private String bucket; private StringMap putPolicy; @Autowired public QiNiuServiceImpl(UploadManager uploadManager, Auth auth) { this.uploadManager = uploadManager; this.auth = auth; } /** * 七牛云上传文件 * * @param file 文件 * @return 七牛上传Response * @throws QiniuException 七牛异常 */ @Override public Response uploadFile(File file) throws QiniuException { Response response = this.uploadManager.put(file, file.getName(), getUploadToken()); int retry = 0; while (response.needRetry() && retry < 3) { response = this.uploadManager.put(file, file.getName(), getUploadToken()); retry++; } return response; } @Override public void afterPropertiesSet() { this.putPolicy = new StringMap(); putPolicy.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"width\":$(imageInfo.width), \"height\":${imageInfo.height}}"); } /** * 获取上传凭证 * * @return 上传凭证 */ private String getUploadToken() { return this.auth.uploadToken(bucket, null, 3600, putPolicy); } } ================================================ FILE: demo-upload/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo qiniu: ## 此处填写你自己的七牛云 access key accessKey: ## 此处填写你自己的七牛云 secret key secretKey: ## 此处填写你自己的七牛云 bucket bucket: ## 此处填写你自己的七牛云 域名 prefix: spring: servlet: multipart: enabled: true location: /Users/yangkai.shen/Documents/code/back-end/spring-boot-demo/spring-boot-demo-upload/tmp file-size-threshold: 5MB max-file-size: 20MB ================================================ FILE: demo-upload/src/main/resources/templates/index.html ================================================ spring-boot-demo-upload

    本地上传

    选择文件 {{ local.loadingStatus ? '本地文件上传中' : '本地上传' }}
    状态:{{local.log.message}}
    文件名:{{local.log.fileName}}
    文件路径:{{local.log.filePath}}

    七牛云上传

    选择文件 {{ yun.loadingStatus ? '七牛云文件上传中' : '七牛云上传' }}
    状态:{{yun.log.message}}
    文件名:{{yun.log.fileName}}
    文件路径:{{yun.log.filePath}}
    ================================================ FILE: demo-upload/src/test/java/com/xkcoding/upload/SpringBootDemoUploadApplicationTests.java ================================================ package com.xkcoding.upload; 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 SpringBootDemoUploadApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-ureport2/.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-ureport2/README.md ================================================ # spring-boot-demo-ureport2 > 本 demo 主要演示了 Spring Boot 项目如何快速集成 ureport2 实现任意复杂的中国式报表功能。 UReport2 是一款基于架构在 Spring 之上纯 Java 的高性能报表引擎,通过迭代单元格可以实现任意复杂的中国式报表。 在 UReport2 中,提供了全新的基于网页的报表设计器,可以在 Chrome、Firefox、Edge 等各种主流浏览器运行(IE 浏览器除外)。使用 UReport2,打开浏览器即可完成各种复杂报表的设计制作。 ## 1. 主要代码 因为官方没有提供一个 starter 包,需要自己集成,这里使用 [pig](https://github.com/pig-mesh/pig) 作者 [冷冷同学](https://github.com/lltx) 开发的 starter 偷懒实现,这个 starter 不仅支持单机环境的配置,同时支持集群环境。 ### 1.1. 单机使用 #### 1.1.1. `pom.xml` 新增依赖 ```xml com.pig4cloud.plugin ureport-spring-boot-starter 0.0.1 ``` #### 1.1.2. `application.yml` 修改配置文件 ```yaml 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&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver ureport: debug: false disableFileProvider: false disableHttpSessionReportCache: true # 单机模式,本地路径需要提前创建 fileStoreDir: '/Users/yk.shen/Desktop/ureport2' ``` #### 1.1.3. 新增一个内部数据源 ```java @Component public class InnerDatasource implements BuildinDatasource { @Autowired private DataSource datasource; @Override public String name() { return "内部数据源"; } @SneakyThrows @Override public Connection getConnection() { return datasource.getConnection(); } } ``` #### 1.1.4. 使用 `doc/sql/t_user_ureport2.sql` 初始化数据 ```mysql DROP TABLE IF EXISTS `t_user_ureport2`; CREATE TABLE `t_user_ureport2` ( `id` bigint(13) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '姓名', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `status` tinyint(4) NOT NULL COMMENT '是否禁用', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; BEGIN; INSERT INTO `t_user_ureport2` VALUES (1, '测试人员 1', '2020-10-22 09:01:58', 1); INSERT INTO `t_user_ureport2` VALUES (2, '测试人员 2', '2020-10-22 09:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (3, '测试人员 3', '2020-10-23 03:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (4, '测试人员 4', '2020-10-23 23:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (5, '测试人员 5', '2020-10-23 23:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (6, '测试人员 6', '2020-10-24 11:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (7, '测试人员 7', '2020-10-24 20:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (8, '测试人员 8', '2020-10-25 08:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (9, '测试人员 9', '2020-10-25 09:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (10, '测试人员 10', '2020-10-25 13:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (11, '测试人员 11', '2020-10-26 21:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (12, '测试人员 12', '2020-10-26 23:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (13, '测试人员 13', '2020-10-26 23:02:00', 1); COMMIT; ``` #### 1.1.5. 访问报表设计器 http://127.0.0.1:8080/demo/ureport/designer ![报表设计页](http://static.xkcoding.com/spring-boot-demo/ureport2/035330.png) #### 1.1.6. 开始设计 ##### 1.1.6.1. 选择数据源 这里就需要使用到上面步骤 1.1.3 创建的内部数据源如图 ![选择数据源](http://static.xkcoding.com/spring-boot-demo/ureport2/040032.png) 选择数据源 ![选择数据源](http://static.xkcoding.com/spring-boot-demo/ureport2/040117.png) 此时列表里就会出现数据源 ![数据源列表](http://static.xkcoding.com/spring-boot-demo/ureport2/040237.png) ##### 1.1.6.2. 选择数据集 在刚才选中的数据源右键,选择添加数据集 ![选中数据源右键](http://static.xkcoding.com/spring-boot-demo/ureport2/063315.png) 这里选择上面步骤 1.1.4 中初始化的用户表 ![创建用户报表](http://static.xkcoding.com/spring-boot-demo/ureport2/063845.png) 预览数据看一下 ![预览数据集数据](http://static.xkcoding.com/spring-boot-demo/ureport2/063955.png) 点击确定,保存数据集 ![保存数据集](http://static.xkcoding.com/spring-boot-demo/ureport2/064049.png) ##### 1.1.6.3. 报表设计 创建报表表头的位置 ![合并单元格](http://static.xkcoding.com/spring-boot-demo/ureport2/064425.png) 表头内容 ![image-20201124144752390](http://static.xkcoding.com/spring-boot-demo/ureport2/064752.png) 操作完成之后,长这样~ ![表头美化](http://static.xkcoding.com/spring-boot-demo/ureport2/064916.png) 然后设置数据的标题行,跟表头设置一样,效果如下图 ![数据的标题行](http://static.xkcoding.com/spring-boot-demo/ureport2/065125.png) 接下来设置数据 ![id字段配置](http://static.xkcoding.com/spring-boot-demo/ureport2/065658.png) 其他字段同理,完成之后如下 ![数据配置](http://static.xkcoding.com/spring-boot-demo/ureport2/070440.png) 此时你可以尝试预览一下数据了 ![预览数据](http://static.xkcoding.com/spring-boot-demo/ureport2/070634.png) ![预览数据](http://static.xkcoding.com/spring-boot-demo/ureport2/070813.png) 关掉,稍微美化一下 ![美化后的预览数据](http://static.xkcoding.com/spring-boot-demo/ureport2/070910.png) 此时数据虽然正常显示了,但是「是否可用」这一列显示0/1 是否可以支持自定义呢? ![映射数据集](http://static.xkcoding.com/spring-boot-demo/ureport2/071352.png) 再次预览一下 ![字典映射预览数据](http://static.xkcoding.com/spring-boot-demo/ureport2/071428.png) 顺带再把创建时间的数据格式也改一下 ![时间格式修改](http://static.xkcoding.com/spring-boot-demo/ureport2/072725.png) 修改后,预览数据如下 ![预览数据](http://static.xkcoding.com/spring-boot-demo/ureport2/072753.png) ##### 1.1.6.4. 保存报表设计文件 ![image-20201124153244035](http://static.xkcoding.com/spring-boot-demo/ureport2/073244.png) ![保存](http://static.xkcoding.com/spring-boot-demo/ureport2/074228.png) 点击保存之后,你本地在 `application.yml` 文件中配置的地址就会出现一个 `demo.ureport.xml` 文件 下次可以直接通过 http://localhost:8080/demo/ureport/preview?_u=file:demo.ureport.xml 这个地址预览报表了 ##### 1.1.6.5. 增加报表查询条件 还记得我们上面新增数据集的时候,加的条件吗?现在用起来 ![查询表单设计器](http://static.xkcoding.com/spring-boot-demo/ureport2/074641.png) 查询表单设计 ![拖动元素设计表单查询](http://static.xkcoding.com/spring-boot-demo/ureport2/074936.png) 配置查询参数 ![完善查询表单](http://static.xkcoding.com/spring-boot-demo/ureport2/075248.png) 美化按钮 ![按钮样式美化](http://static.xkcoding.com/spring-boot-demo/ureport2/075410.png) 在预览一下~ ![预览数据-查询条件](http://static.xkcoding.com/spring-boot-demo/ureport2/075640.png) ### 1.2. 集群使用 如上文设计好的模板是保存在服务本机的,在集群环境中需要使用统一的文件系统存储。 #### 1.2.1. 新增依赖 ```xml com.pig4cloud.plugin oss-spring-boot-starter 0.0.3 ``` #### 1.2.2. 仅需配置云存储相关参数, 演示为minio ```yaml oss: access-key: lengleng secret-key: lengleng bucket-name: lengleng endpoint: http://minio.pig4cloud.com ``` > 注意:这里使用的是冷冷提供的公共 minio,请勿乱用,也不保证数据的可靠性,建议小伙伴自建一个minio,或者使用阿里云 oss ## 2. 坑 Ureport2 最新版本是 `2.2.9`,挺久没更新了,存在一个坑:在报表设计页打开一个已存在的报表设计文件时,可能会出现无法预览的情况,参考 ISSUE:https://github.com/youseries/ureport/issues/393 ![无法预览](http://static.xkcoding.com/spring-boot-demo/ureport2/084852.png) 解决办法: ![image-20201124164953947](http://static.xkcoding.com/spring-boot-demo/ureport2/084954.png) 条件表达式变成 `undefined`,这里需要注意的是,我们的 xml 文件是正常的,只不过是 ureport 解析的时候出错了。 ![条件表达式](http://static.xkcoding.com/spring-boot-demo/ureport2/085114.png) 点击编辑,重新选择表达式即可解决 ![image-20201124165202295](http://static.xkcoding.com/spring-boot-demo/ureport2/085202.png) 再次尝试预览 ![斑马纹预览数据](http://static.xkcoding.com/spring-boot-demo/ureport2/085228.png) > 注意:该可能性出现在报表设计文件中使用了条件属性的情况下,修复方法就是打开文件之后,重新配置条件属性,此处是坑,小伙伴使用时注意下就好,最好的方法就是避免使用条件属性。 ## 3. 感谢 再次感谢 [@冷冷](https://github.com/lltx) 提供的 starter 及 PR,因个人操作失误,PR 未被合并,抱歉~ ## 4. 参考 - [ureport2 使用文档](https://www.w3cschool.cn/ureport) - [ureport-spring-boot-starter](https://github.com/pig-mesh/ureport-spring-boot-starter) UReport2 的 spring boot 封装 - [oss-spring-boot-starter](https://github.com/pig-mesh/oss-spring-boot-starter) 兼容所有 S3 协议的分布式文件存储系统 ================================================ FILE: demo-ureport2/doc/sql/t_user_ureport2.sql ================================================ /* Navicat Premium Data Transfer Source Server : dev Source Server Type : MySQL Source Server Version : 50732 Source Host : localhost:3306 Source Schema : spring-boot-demo Target Server Type : MySQL Target Server Version : 50732 File Encoding : 65001 Date: 26/10/2020 23:30:27 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_user_ureport2 -- ---------------------------- DROP TABLE IF EXISTS `t_user_ureport2`; CREATE TABLE `t_user_ureport2` ( `id` bigint(13) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '姓名', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `status` tinyint(4) NOT NULL COMMENT '是否禁用', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; -- ---------------------------- -- Records of t_user_ureport2 -- ---------------------------- BEGIN; INSERT INTO `t_user_ureport2` VALUES (1, '测试人员 1', '2020-10-22 09:01:58', 1); INSERT INTO `t_user_ureport2` VALUES (2, '测试人员 2', '2020-10-22 09:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (3, '测试人员 3', '2020-10-23 03:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (4, '测试人员 4', '2020-10-23 23:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (5, '测试人员 5', '2020-10-23 23:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (6, '测试人员 6', '2020-10-24 11:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (7, '测试人员 7', '2020-10-24 20:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (8, '测试人员 8', '2020-10-25 08:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (9, '测试人员 9', '2020-10-25 09:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (10, '测试人员 10', '2020-10-25 13:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (11, '测试人员 11', '2020-10-26 21:02:00', 0); INSERT INTO `t_user_ureport2` VALUES (12, '测试人员 12', '2020-10-26 23:02:00', 1); INSERT INTO `t_user_ureport2` VALUES (13, '测试人员 13', '2020-10-26 23:02:00', 1); COMMIT; SET FOREIGN_KEY_CHECKS = 1; ================================================ FILE: demo-ureport2/doc/ureport2/user_inner_datasource.ureport.xml ================================================ ================================================ FILE: demo-ureport2/pom.xml ================================================ 4.0.0 demo-ureport2 1.0.0-SNAPSHOT jar demo-ureport2 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-data-jpa mysql mysql-connector-java com.pig4cloud.plugin ureport-spring-boot-starter 0.0.1 org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true demo-ureport2 org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-ureport2/src/main/java/com/xkcoding/ureport2/SpringBootDemoUreport2Application.java ================================================ package com.xkcoding.ureport2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-02-26 23:56 */ @SpringBootApplication public class SpringBootDemoUreport2Application { public static void main(String[] args) { SpringApplication.run(SpringBootDemoUreport2Application.class, args); } } ================================================ FILE: demo-ureport2/src/main/java/com/xkcoding/ureport2/config/InnerDatasource.java ================================================ package com.xkcoding.ureport2.config; import com.bstek.ureport.definition.datasource.BuildinDatasource; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; /** *

    * 内部数据源 *

    * * @author yangkai.shen * @date Created in 2020-10-26 22:32 */ @Component public class InnerDatasource implements BuildinDatasource { @Autowired private DataSource datasource; @Override public String name() { return "内部数据源"; } @SneakyThrows @Override public Connection getConnection() { return datasource.getConnection(); } } ================================================ FILE: demo-ureport2/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&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver ureport: debug: false disableFileProvider: false disableHttpSessionReportCache: true # 单机模式,本地路径需要提前创建 fileStoreDir: '/Users/yk.shen/Desktop/ureport2' #oss: # access-key: lengleng # secret-key: lengleng # bucket-name: lengleng # endpoint: http://minio.pig4cloud.com ================================================ FILE: demo-ureport2/src/test/java/com/xkcoding/ureport2/SpringBootDemoUreport2ApplicationTests.java ================================================ package com.xkcoding.ureport2; 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 SpringBootDemoUreport2ApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-urule/.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-urule/pom.xml ================================================ 4.0.0 demo-urule 1.0.0-SNAPSHOT jar demo-urule 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-urule org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-urule/src/main/java/com/xkcoding/urule/SpringBootDemoUruleApplication.java ================================================ package com.xkcoding.urule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2019-02-25 22:46 */ @SpringBootApplication public class SpringBootDemoUruleApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoUruleApplication.class, args); } } ================================================ FILE: demo-urule/src/main/resources/application.properties ================================================ ================================================ FILE: demo-urule/src/test/java/com/xkcoding/urule/SpringBootDemoUruleApplicationTests.java ================================================ package com.xkcoding.urule; 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 SpringBootDemoUruleApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-war/.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-war/README.md ================================================ # spring-boot-demo-war > 本 demo 主要演示了如何将 Spring Boot 项目打包成传统的 war 包程序。 ## pom.xml ```xml 4.0.0 spring-boot-demo-war 1.0.0-SNAPSHOT war spring-boot-demo-war 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 provided org.springframework.boot spring-boot-starter-test test spring-boot-demo-war org.springframework.boot spring-boot-maven-plugin ``` ## SpringBootDemoWarApplication.java ```java /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-10-30 19:37 */ @SpringBootApplication public class SpringBootDemoWarApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(SpringBootDemoWarApplication.class, args); } /** * 若需要打成 war 包,则需要写一个类继承 {@link SpringBootServletInitializer} 并重写 {@link SpringBootServletInitializer#configure(SpringApplicationBuilder)} */ @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(SpringBootDemoWarApplication.class); } } ``` ## 参考 https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#howto-create-a-deployable-war-file ================================================ FILE: demo-war/pom.xml ================================================ 4.0.0 demo-war 1.0.0-SNAPSHOT war demo-war 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 provided org.springframework.boot spring-boot-starter-test test demo-war org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-war-plugin 2.6 false ================================================ FILE: demo-war/src/main/java/com/xkcoding/war/SpringBootDemoWarApplication.java ================================================ package com.xkcoding.war; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-10-30 19:37 */ @SpringBootApplication public class SpringBootDemoWarApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(SpringBootDemoWarApplication.class, args); } /** * 若需要打成 war 包,则需要写一个类继承 {@link SpringBootServletInitializer} 并重写 {@link SpringBootServletInitializer#configure(SpringApplicationBuilder)} */ @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(SpringBootDemoWarApplication.class); } } ================================================ FILE: demo-war/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ================================================ FILE: demo-war/src/test/java/com/xkcoding/war/SpringBootDemoWarApplicationTests.java ================================================ package com.xkcoding.war; 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 SpringBootDemoWarApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-websocket/.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-websocket/README.md ================================================ # spring-boot-demo-websocket > 此 demo 主要演示了 Spring Boot 如何集成 WebSocket,实现后端主动往前端推送数据。网上大部分websocket的例子都是聊天室,本例主要是推送服务器状态信息。前端页面基于vue和element-ui实现。 ## 1. 代码 ### 1.1. pom.xml ```xml 4.0.0 spring-boot-demo-websocket 1.0.0-SNAPSHOT spring-boot-demo-websocket Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 3.9.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-test test com.github.oshi oshi-core ${oshi.version} cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true spring-boot-demo-websocket org.springframework.boot spring-boot-maven-plugin ``` ### 1.2. WebSocketConfig.java ```java /** *

    * WebSocket配置 *

    * * @author yangkai.shen * @date Created in 2018-12-14 15:58 */ @Configuration @EnableWebSocket @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册一个 /notification 端点,前端通过这个端点进行连接 registry.addEndpoint("/notification") //解决跨域问题 .setAllowedOrigins("*") .withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息 registry.enableSimpleBroker("/topic"); } } ``` ### 1.3. 服务器相关实体 > 此部分实体 参见包路径 [com.xkcoding.websocket.model](./src/main/java/com/xkcoding/websocket/model) ### 1.4. ServerTask.java ```java /** *

    * 服务器定时推送任务 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:04 */ @Slf4j @Component public class ServerTask { @Autowired private SimpMessagingTemplate wsTemplate; /** * 按照标准时间来算,每隔 2s 执行一次 */ @Scheduled(cron = "0/2 * * * * ?") public void websocket() throws Exception { log.info("【推送消息】开始执行:{}", DateUtil.formatDateTime(new Date())); // 查询服务器状态 Server server = new Server(); server.copyTo(); ServerVO serverVO = ServerUtil.wrapServerVO(server); Dict dict = ServerUtil.wrapServerDict(serverVO); wsTemplate.convertAndSend(WebSocketConsts.PUSH_SERVER, JSONUtil.toJsonStr(dict)); log.info("【推送消息】执行结束:{}", DateUtil.formatDateTime(new Date())); } } ``` ### 1.5. server.html ```html 服务器信息
    手动连接 断开连接
    CPU信息
    内存信息
    服务器信息
    Java虚拟机信息
    磁盘状态
    ``` ## 2. 运行方式 1. 启动 `SpringBootDemoWebsocketApplication.java` 2. 访问 http://localhost:8080/demo/server.html ## 3. 运行效果 ![image-20181217110240322](http://static.xkcoding.com/spring-boot-demo/websocket/064107.jpg) ![image-20181217110304065](http://static.xkcoding.com/spring-boot-demo/websocket/064108.jpg) ![image-20181217110328810](http://static.xkcoding.com/spring-boot-demo/websocket/064109.jpg) ![image-20181217110336017](http://static.xkcoding.com/spring-boot-demo/websocket/064109-1.jpg) ## 4. 参考 ### 4.1. 后端 1. Spring Boot 整合 Websocket 官方文档:https://docs.spring.io/spring/docs/5.1.2.RELEASE/spring-framework-reference/web.html#websocket 2. 服务器信息采集 oshi 使用:https://github.com/oshi/oshi ### 4.2. 前端 1. vue.js 语法:https://cn.vuejs.org/v2/guide/ 2. element-ui 用法:http://element-cn.eleme.io/#/zh-CN 3. stomp.js 用法:https://github.com/jmesnil/stomp-websocket 4. sockjs 用法:https://github.com/sockjs/sockjs-client 5. axios.js 用法:https://github.com/axios/axios#example ================================================ FILE: demo-websocket/pom.xml ================================================ 4.0.0 demo-websocket 1.0.0-SNAPSHOT demo-websocket Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 3.9.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-test test com.github.oshi oshi-core ${oshi.version} cn.hutool hutool-all com.google.guava guava org.projectlombok lombok true demo-websocket org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/SpringBootDemoWebsocketApplication.java ================================================ package com.xkcoding.websocket; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-12-14 14:58 */ @SpringBootApplication @EnableScheduling public class SpringBootDemoWebsocketApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoWebsocketApplication.class, args); } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/common/WebSocketConsts.java ================================================ package com.xkcoding.websocket.common; /** *

    * WebSocket常量 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:01 */ public interface WebSocketConsts { String PUSH_SERVER = "/topic/server"; } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/config/WebSocketConfig.java ================================================ package com.xkcoding.websocket.config; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; /** *

    * WebSocket配置 *

    * * @author yangkai.shen * @date Created in 2018-12-14 15:58 */ @Configuration @EnableWebSocket @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册一个 /notification 端点,前端通过这个端点进行连接 registry.addEndpoint("/notification") //解决跨域问题 .setAllowedOrigins("*").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息 registry.enableSimpleBroker("/topic"); } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/controller/ServerController.java ================================================ package com.xkcoding.websocket.controller; import cn.hutool.core.lang.Dict; import com.xkcoding.websocket.model.Server; import com.xkcoding.websocket.payload.ServerVO; import com.xkcoding.websocket.util.ServerUtil; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** *

    * 服务器监控Controller *

    * * @author yangkai.shen * @date Created in 2018-12-17 10:22 */ @RestController @RequestMapping("/server") public class ServerController { @GetMapping public Dict serverInfo() throws Exception { Server server = new Server(); server.copyTo(); ServerVO serverVO = ServerUtil.wrapServerVO(server); return ServerUtil.wrapServerDict(serverVO); } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/model/Server.java ================================================ package com.xkcoding.websocket.model; import cn.hutool.core.util.NumberUtil; import com.xkcoding.websocket.model.server.*; import com.xkcoding.websocket.util.IpUtil; import oshi.SystemInfo; import oshi.hardware.CentralProcessor; import oshi.hardware.CentralProcessor.TickType; import oshi.hardware.GlobalMemory; import oshi.hardware.HardwareAbstractionLayer; import oshi.software.os.FileSystem; import oshi.software.os.OSFileStore; import oshi.software.os.OperatingSystem; import oshi.util.Util; import java.net.UnknownHostException; import java.util.LinkedList; import java.util.List; import java.util.Properties; /** *

    * 服务器相关信息实体 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:09 */ public class Server { private static final int OSHI_WAIT_SECOND = 1000; /** * CPU相关信息 */ private Cpu cpu = new Cpu(); /** * 內存相关信息 */ private Mem mem = new Mem(); /** * JVM相关信息 */ private Jvm jvm = new Jvm(); /** * 服务器相关信息 */ private Sys sys = new Sys(); /** * 磁盘相关信息 */ private List sysFiles = new LinkedList(); public Cpu getCpu() { return cpu; } public void setCpu(Cpu cpu) { this.cpu = cpu; } public Mem getMem() { return mem; } public void setMem(Mem mem) { this.mem = mem; } public Jvm getJvm() { return jvm; } public void setJvm(Jvm jvm) { this.jvm = jvm; } public Sys getSys() { return sys; } public void setSys(Sys sys) { this.sys = sys; } public List getSysFiles() { return sysFiles; } public void setSysFiles(List sysFiles) { this.sysFiles = sysFiles; } public void copyTo() throws Exception { SystemInfo si = new SystemInfo(); HardwareAbstractionLayer hal = si.getHardware(); setCpuInfo(hal.getProcessor()); setMemInfo(hal.getMemory()); setSysInfo(); setJvmInfo(); setSysFiles(si.getOperatingSystem()); } /** * 设置CPU信息 */ private void setCpuInfo(CentralProcessor processor) { // CPU信息 long[] prevTicks = processor.getSystemCpuLoadTicks(); Util.sleep(OSHI_WAIT_SECOND); long[] ticks = processor.getSystemCpuLoadTicks(); long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; cpu.setCpuNum(processor.getLogicalProcessorCount()); cpu.setTotal(totalCpu); cpu.setSys(cSys); cpu.setUsed(user); cpu.setWait(iowait); cpu.setFree(idle); } /** * 设置内存信息 */ private void setMemInfo(GlobalMemory memory) { mem.setTotal(memory.getTotal()); mem.setUsed(memory.getTotal() - memory.getAvailable()); mem.setFree(memory.getAvailable()); } /** * 设置服务器信息 */ private void setSysInfo() { Properties props = System.getProperties(); sys.setComputerName(IpUtil.getHostName()); sys.setComputerIp(IpUtil.getHostIp()); sys.setOsName(props.getProperty("os.name")); sys.setOsArch(props.getProperty("os.arch")); sys.setUserDir(props.getProperty("user.dir")); } /** * 设置Java虚拟机 */ private void setJvmInfo() throws UnknownHostException { Properties props = System.getProperties(); jvm.setTotal(Runtime.getRuntime().totalMemory()); jvm.setMax(Runtime.getRuntime().maxMemory()); jvm.setFree(Runtime.getRuntime().freeMemory()); jvm.setVersion(props.getProperty("java.version")); jvm.setHome(props.getProperty("java.home")); } /** * 设置磁盘信息 */ private void setSysFiles(OperatingSystem os) { FileSystem fileSystem = os.getFileSystem(); OSFileStore[] fsArray = fileSystem.getFileStores(); for (OSFileStore fs : fsArray) { long free = fs.getUsableSpace(); long total = fs.getTotalSpace(); long used = total - free; SysFile sysFile = new SysFile(); sysFile.setDirName(fs.getMount()); sysFile.setSysTypeName(fs.getType()); sysFile.setTypeName(fs.getName()); sysFile.setTotal(convertFileSize(total)); sysFile.setFree(convertFileSize(free)); sysFile.setUsed(convertFileSize(used)); sysFile.setUsage(NumberUtil.mul(NumberUtil.div(used, total, 4), 100)); sysFiles.add(sysFile); } } /** * 字节转换 * * @param size 字节大小 * @return 转换后值 */ public String convertFileSize(long size) { long kb = 1024; long mb = kb * 1024; long gb = mb * 1024; if (size >= gb) { return String.format("%.1f GB", (float) size / gb); } else if (size >= mb) { float f = (float) size / mb; return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); } else if (size >= kb) { float f = (float) size / kb; return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); } else { return String.format("%d B", size); } } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/model/server/Cpu.java ================================================ package com.xkcoding.websocket.model.server; import cn.hutool.core.util.NumberUtil; /** *

    * CPU相关信息实体 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:09 */ public class Cpu { /** * 核心数 */ private int cpuNum; /** * CPU总的使用率 */ private double total; /** * CPU系统使用率 */ private double sys; /** * CPU用户使用率 */ private double used; /** * CPU当前等待率 */ private double wait; /** * CPU当前空闲率 */ private double free; public int getCpuNum() { return cpuNum; } public void setCpuNum(int cpuNum) { this.cpuNum = cpuNum; } public double getTotal() { return NumberUtil.round(NumberUtil.mul(total, 100), 2).doubleValue(); } public void setTotal(double total) { this.total = total; } public double getSys() { return NumberUtil.round(NumberUtil.mul(sys / total, 100), 2).doubleValue(); } public void setSys(double sys) { this.sys = sys; } public double getUsed() { return NumberUtil.round(NumberUtil.mul(used / total, 100), 2).doubleValue(); } public void setUsed(double used) { this.used = used; } public double getWait() { return NumberUtil.round(NumberUtil.mul(wait / total, 100), 2).doubleValue(); } public void setWait(double wait) { this.wait = wait; } public double getFree() { return NumberUtil.round(NumberUtil.mul(free / total, 100), 2).doubleValue(); } public void setFree(double free) { this.free = free; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/model/server/Jvm.java ================================================ package com.xkcoding.websocket.model.server; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.NumberUtil; import java.lang.management.ManagementFactory; import java.util.Date; /** *

    * JVM相关信息实体 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:09 */ public class Jvm { /** * 当前JVM占用的内存总数(M) */ private double total; /** * JVM最大可用内存总数(M) */ private double max; /** * JVM空闲内存(M) */ private double free; /** * JDK版本 */ private String version; /** * JDK路径 */ private String home; /** * JDK启动时间 */ private String startTime; /** * JDK运行时间 */ private String runTime; public double getTotal() { return NumberUtil.div(total, (1024 * 1024), 2); } public void setTotal(double total) { this.total = total; } public double getMax() { return NumberUtil.div(max, (1024 * 1024), 2); } public void setMax(double max) { this.max = max; } public double getFree() { return NumberUtil.div(free, (1024 * 1024), 2); } public void setFree(double free) { this.free = free; } public double getUsed() { return NumberUtil.div(total - free, (1024 * 1024), 2); } public double getUsage() { return NumberUtil.mul(NumberUtil.div(total - free, total, 4), 100); } /** * 获取JDK名称 */ public String getName() { return ManagementFactory.getRuntimeMXBean().getVmName(); } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getHome() { return home; } public void setHome(String home) { this.home = home; } public void setStartTime(String startTime) { this.startTime = startTime; } public String getStartTime() { return DateUtil.formatDateTime(new Date(ManagementFactory.getRuntimeMXBean().getStartTime())); } public void setRunTime(String runTime) { this.runTime = runTime; } public String getRunTime() { long startTime = ManagementFactory.getRuntimeMXBean().getStartTime(); return DateUtil.formatBetween(DateUtil.current(false) - startTime); } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/model/server/Mem.java ================================================ package com.xkcoding.websocket.model.server; import cn.hutool.core.util.NumberUtil; /** *

    * 內存相关信息实体 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:09 */ public class Mem { /** * 内存总量 */ private double total; /** * 已用内存 */ private double used; /** * 剩余内存 */ private double free; public double getTotal() { return NumberUtil.div(total, (1024 * 1024 * 1024), 2); } public void setTotal(long total) { this.total = total; } public double getUsed() { return NumberUtil.div(used, (1024 * 1024 * 1024), 2); } public void setUsed(long used) { this.used = used; } public double getFree() { return NumberUtil.div(free, (1024 * 1024 * 1024), 2); } public void setFree(long free) { this.free = free; } public double getUsage() { return NumberUtil.mul(NumberUtil.div(used, total, 4), 100); } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/model/server/Sys.java ================================================ package com.xkcoding.websocket.model.server; /** *

    * 系统相关信息实体 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:10 */ public class Sys { /** * 服务器名称 */ private String computerName; /** * 服务器Ip */ private String computerIp; /** * 项目路径 */ private String userDir; /** * 操作系统 */ private String osName; /** * 系统架构 */ private String osArch; public String getComputerName() { return computerName; } public void setComputerName(String computerName) { this.computerName = computerName; } public String getComputerIp() { return computerIp; } public void setComputerIp(String computerIp) { this.computerIp = computerIp; } public String getUserDir() { return userDir; } public void setUserDir(String userDir) { this.userDir = userDir; } public String getOsName() { return osName; } public void setOsName(String osName) { this.osName = osName; } public String getOsArch() { return osArch; } public void setOsArch(String osArch) { this.osArch = osArch; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/model/server/SysFile.java ================================================ package com.xkcoding.websocket.model.server; /** *

    * 系统文件相关信息实体 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:10 */ public class SysFile { /** * 盘符路径 */ private String dirName; /** * 盘符类型 */ private String sysTypeName; /** * 文件类型 */ private String typeName; /** * 总大小 */ private String total; /** * 剩余大小 */ private String free; /** * 已经使用量 */ private String used; /** * 资源的使用率 */ private double usage; public String getDirName() { return dirName; } public void setDirName(String dirName) { this.dirName = dirName; } public String getSysTypeName() { return sysTypeName; } public void setSysTypeName(String sysTypeName) { this.sysTypeName = sysTypeName; } public String getTypeName() { return typeName; } public void setTypeName(String typeName) { this.typeName = typeName; } public String getTotal() { return total; } public void setTotal(String total) { this.total = total; } public String getFree() { return free; } public void setFree(String free) { this.free = free; } public String getUsed() { return used; } public void setUsed(String used) { this.used = used; } public double getUsage() { return usage; } public void setUsage(double usage) { this.usage = usage; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/KV.java ================================================ package com.xkcoding.websocket.payload; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** *

    * 键值匹配 *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:41 */ @Data @AllArgsConstructor @NoArgsConstructor public class KV { /** * 键 */ private String key; /** * 值 */ private Object value; } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/ServerVO.java ================================================ package com.xkcoding.websocket.payload; import com.google.common.collect.Lists; import com.xkcoding.websocket.model.Server; import com.xkcoding.websocket.payload.server.*; import lombok.Data; import java.util.List; /** *

    * 服务器信息VO *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:25 */ @Data public class ServerVO { List cpu = Lists.newArrayList(); List jvm = Lists.newArrayList(); List mem = Lists.newArrayList(); List sysFile = Lists.newArrayList(); List sys = Lists.newArrayList(); public ServerVO create(Server server) { cpu.add(CpuVO.create(server.getCpu())); jvm.add(JvmVO.create(server.getJvm())); mem.add(MemVO.create(server.getMem())); sysFile.add(SysFileVO.create(server.getSysFiles())); sys.add(SysVO.create(server.getSys())); return null; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/server/CpuVO.java ================================================ package com.xkcoding.websocket.payload.server; import com.google.common.collect.Lists; import com.xkcoding.websocket.model.server.Cpu; import com.xkcoding.websocket.payload.KV; import lombok.Data; import java.util.List; /** *

    * CPU相关信息实体VO *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:27 */ @Data public class CpuVO { List data = Lists.newArrayList(); public static CpuVO create(Cpu cpu) { CpuVO vo = new CpuVO(); vo.data.add(new KV("核心数", cpu.getCpuNum())); vo.data.add(new KV("CPU总的使用率", cpu.getTotal())); vo.data.add(new KV("CPU系统使用率", cpu.getSys() + "%")); vo.data.add(new KV("CPU用户使用率", cpu.getUsed() + "%")); vo.data.add(new KV("CPU当前等待率", cpu.getWait() + "%")); vo.data.add(new KV("CPU当前空闲率", cpu.getFree() + "%")); return vo; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/server/JvmVO.java ================================================ package com.xkcoding.websocket.payload.server; import com.google.common.collect.Lists; import com.xkcoding.websocket.model.server.Jvm; import com.xkcoding.websocket.payload.KV; import lombok.Data; import java.util.List; /** *

    * JVM相关信息实体VO *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:28 */ @Data public class JvmVO { List data = Lists.newArrayList(); public static JvmVO create(Jvm jvm) { JvmVO vo = new JvmVO(); vo.data.add(new KV("当前JVM占用的内存总数(M)", jvm.getTotal() + "M")); vo.data.add(new KV("JVM最大可用内存总数(M)", jvm.getMax() + "M")); vo.data.add(new KV("JVM空闲内存(M)", jvm.getFree() + "M")); vo.data.add(new KV("JVM使用率", jvm.getUsage() + "%")); vo.data.add(new KV("JDK版本", jvm.getVersion())); vo.data.add(new KV("JDK路径", jvm.getHome())); vo.data.add(new KV("JDK启动时间", jvm.getStartTime())); vo.data.add(new KV("JDK运行时间", jvm.getRunTime())); return vo; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/server/MemVO.java ================================================ package com.xkcoding.websocket.payload.server; import com.google.common.collect.Lists; import com.xkcoding.websocket.model.server.Mem; import com.xkcoding.websocket.payload.KV; import lombok.Data; import java.util.List; /** *

    * 內存相关信息实体VO *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:28 */ @Data public class MemVO { List data = Lists.newArrayList(); public static MemVO create(Mem mem) { MemVO vo = new MemVO(); vo.data.add(new KV("内存总量", mem.getTotal() + "G")); vo.data.add(new KV("已用内存", mem.getUsed() + "G")); vo.data.add(new KV("剩余内存", mem.getFree() + "G")); vo.data.add(new KV("使用率", mem.getUsage() + "%")); return vo; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/server/SysFileVO.java ================================================ package com.xkcoding.websocket.payload.server; import com.google.common.collect.Lists; import com.xkcoding.websocket.model.server.SysFile; import com.xkcoding.websocket.payload.KV; import lombok.Data; import java.util.List; /** *

    * 系统文件相关信息实体VO *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:30 */ @Data public class SysFileVO { List> data = Lists.newArrayList(); public static SysFileVO create(List sysFiles) { SysFileVO vo = new SysFileVO(); for (SysFile sysFile : sysFiles) { List item = Lists.newArrayList(); item.add(new KV("盘符路径", sysFile.getDirName())); item.add(new KV("盘符类型", sysFile.getSysTypeName())); item.add(new KV("文件类型", sysFile.getTypeName())); item.add(new KV("总大小", sysFile.getTotal())); item.add(new KV("剩余大小", sysFile.getFree())); item.add(new KV("已经使用量", sysFile.getUsed())); item.add(new KV("资源的使用率", sysFile.getUsage() + "%")); vo.data.add(item); } return vo; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/payload/server/SysVO.java ================================================ package com.xkcoding.websocket.payload.server; import com.google.common.collect.Lists; import com.xkcoding.websocket.model.server.Sys; import com.xkcoding.websocket.payload.KV; import lombok.Data; import java.util.List; /** *

    * 系统相关信息实体VO *

    * * @author yangkai.shen * @date Created in 2018-12-14 17:28 */ @Data public class SysVO { List data = Lists.newArrayList(); public static SysVO create(Sys sys) { SysVO vo = new SysVO(); vo.data.add(new KV("服务器名称", sys.getComputerName())); vo.data.add(new KV("服务器Ip", sys.getComputerIp())); vo.data.add(new KV("项目路径", sys.getUserDir())); vo.data.add(new KV("操作系统", sys.getOsName())); vo.data.add(new KV("系统架构", sys.getOsArch())); return vo; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/task/ServerTask.java ================================================ package com.xkcoding.websocket.task; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Dict; import cn.hutool.json.JSONUtil; import com.xkcoding.websocket.common.WebSocketConsts; import com.xkcoding.websocket.model.Server; import com.xkcoding.websocket.payload.ServerVO; import com.xkcoding.websocket.util.ServerUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Date; /** *

    * 服务器定时推送任务 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:04 */ @Slf4j @Component public class ServerTask { @Autowired private SimpMessagingTemplate wsTemplate; /** * 按照标准时间来算,每隔 2s 执行一次 */ @Scheduled(cron = "0/2 * * * * ?") public void websocket() throws Exception { log.info("【推送消息】开始执行:{}", DateUtil.formatDateTime(new Date())); // 查询服务器状态 Server server = new Server(); server.copyTo(); ServerVO serverVO = ServerUtil.wrapServerVO(server); Dict dict = ServerUtil.wrapServerDict(serverVO); wsTemplate.convertAndSend(WebSocketConsts.PUSH_SERVER, JSONUtil.toJsonStr(dict)); log.info("【推送消息】执行结束:{}", DateUtil.formatDateTime(new Date())); } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/util/IpUtil.java ================================================ package com.xkcoding.websocket.util; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; /** *

    * IP 工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-14 16:08 */ public class IpUtil { public static String getIpAddr(HttpServletRequest request) { if (request == null) { return "unknown"; } 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("X-Forwarded-For"); } 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.getHeader("X-Real-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; } public static boolean internalIp(String ip) { byte[] addr = textToNumericFormatV4(ip); return internalIp(addr) || "127.0.0.1".equals(ip); } private static boolean internalIp(byte[] addr) { final byte b0 = addr[0]; final byte b1 = addr[1]; // 10.x.x.x/8 final byte SECTION_1 = 0x0A; // 172.16.x.x/12 final byte SECTION_2 = (byte) 0xAC; final byte SECTION_3 = (byte) 0x10; final byte SECTION_4 = (byte) 0x1F; // 192.168.x.x/16 final byte SECTION_5 = (byte) 0xC0; final byte SECTION_6 = (byte) 0xA8; switch (b0) { case SECTION_1: return true; case SECTION_2: if (b1 >= SECTION_3 && b1 <= SECTION_4) { return true; } case SECTION_5: switch (b1) { case SECTION_6: return true; } default: return false; } } /** * 将IPv4地址转换成字节 * * @param text IPv4地址 * @return byte 字节 */ public static byte[] textToNumericFormatV4(String text) { if (text.length() == 0) { return null; } byte[] bytes = new byte[4]; String[] elements = text.split("\\.", -1); try { long l; int i; switch (elements.length) { case 1: l = Long.parseLong(elements[0]); if ((l < 0L) || (l > 4294967295L)) { return null; } bytes[0] = (byte) (int) (l >> 24 & 0xFF); bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 2: l = Integer.parseInt(elements[0]); if ((l < 0L) || (l > 255L)) { return null; } bytes[0] = (byte) (int) (l & 0xFF); l = Integer.parseInt(elements[1]); if ((l < 0L) || (l > 16777215L)) { return null; } bytes[1] = (byte) (int) (l >> 16 & 0xFF); bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 3: for (i = 0; i < 2; ++i) { l = Integer.parseInt(elements[i]); if ((l < 0L) || (l > 255L)) { return null; } bytes[i] = (byte) (int) (l & 0xFF); } l = Integer.parseInt(elements[2]); if ((l < 0L) || (l > 65535L)) { return null; } bytes[2] = (byte) (int) (l >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 4: for (i = 0; i < 4; ++i) { l = Integer.parseInt(elements[i]); if ((l < 0L) || (l > 255L)) { return null; } bytes[i] = (byte) (int) (l & 0xFF); } break; default: return null; } } catch (NumberFormatException e) { return null; } return bytes; } public static String getHostIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { } return "127.0.0.1"; } public static String getHostName() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { } return "未知"; } } ================================================ FILE: demo-websocket/src/main/java/com/xkcoding/websocket/util/ServerUtil.java ================================================ package com.xkcoding.websocket.util; import cn.hutool.core.lang.Dict; import com.xkcoding.websocket.model.Server; import com.xkcoding.websocket.payload.ServerVO; /** *

    * 服务器转换工具类 *

    * * @author yangkai.shen * @date Created in 2018-12-17 10:24 */ public class ServerUtil { /** * 包装成 ServerVO * * @param server server * @return ServerVO */ public static ServerVO wrapServerVO(Server server) { ServerVO serverVO = new ServerVO(); serverVO.create(server); return serverVO; } /** * 包装成 Dict * * @param serverVO serverVO * @return Dict */ public static Dict wrapServerDict(ServerVO serverVO) { Dict dict = Dict.create().set("cpu", serverVO.getCpu().get(0).getData()).set("mem", serverVO.getMem().get(0).getData()).set("sys", serverVO.getSys().get(0).getData()).set("jvm", serverVO.getJvm().get(0).getData()).set("sysFile", serverVO.getSysFile().get(0).getData()); return dict; } } ================================================ FILE: demo-websocket/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ================================================ FILE: demo-websocket/src/main/resources/static/js/stomp.js ================================================ // Generated by CoffeeScript 1.7.1 /* Stomp Over WebSocket http://www.jmesnil.net/stomp-websocket/doc/ | Apache License V2.0 Copyright (C) 2010-2013 [Jeff Mesnil](http://jmesnil.net/) Copyright (C) 2012 [FuseSource, Inc.](http://fusesource.com) */ (function() { var Byte, Client, Frame, Stomp, __hasProp = {}.hasOwnProperty, __slice = [].slice; Byte = { LF: '\x0A', NULL: '\x00' }; Frame = (function() { var unmarshallSingle; function Frame(command, headers, body) { this.command = command; this.headers = headers != null ? headers : {}; this.body = body != null ? body : ''; } Frame.prototype.toString = function() { var lines, name, skipContentLength, value, _ref; lines = [this.command]; skipContentLength = this.headers['content-length'] === false ? true : false; if (skipContentLength) { delete this.headers['content-length']; } _ref = this.headers; for (name in _ref) { if (!__hasProp.call(_ref, name)) continue; value = _ref[name]; lines.push("" + name + ":" + value); } if (this.body && !skipContentLength) { lines.push("content-length:" + (Frame.sizeOfUTF8(this.body))); } lines.push(Byte.LF + this.body); return lines.join(Byte.LF); }; Frame.sizeOfUTF8 = function(s) { if (s) { return encodeURI(s).match(/%..|./g).length; } else { return 0; } }; unmarshallSingle = function(data) { var body, chr, command, divider, headerLines, headers, i, idx, len, line, start, trim, _i, _j, _len, _ref, _ref1; divider = data.search(RegExp("" + Byte.LF + Byte.LF)); headerLines = data.substring(0, divider).split(Byte.LF); command = headerLines.shift(); headers = {}; trim = function(str) { return str.replace(/^\s+|\s+$/g, ''); }; _ref = headerLines.reverse(); for (_i = 0, _len = _ref.length; _i < _len; _i++) { line = _ref[_i]; idx = line.indexOf(':'); headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1)); } body = ''; start = divider + 2; if (headers['content-length']) { len = parseInt(headers['content-length']); body = ('' + data).substring(start, start + len); } else { chr = null; for (i = _j = start, _ref1 = data.length; start <= _ref1 ? _j < _ref1 : _j > _ref1; i = start <= _ref1 ? ++_j : --_j) { chr = data.charAt(i); if (chr === Byte.NULL) { break; } body += chr; } } return new Frame(command, headers, body); }; Frame.unmarshall = function(datas) { var frame, frames, last_frame, r; frames = datas.split(RegExp("" + Byte.NULL + Byte.LF + "*")); r = { frames: [], partial: '' }; r.frames = (function() { var _i, _len, _ref, _results; _ref = frames.slice(0, -1); _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { frame = _ref[_i]; _results.push(unmarshallSingle(frame)); } return _results; })(); last_frame = frames.slice(-1)[0]; if (last_frame === Byte.LF || (last_frame.search(RegExp("" + Byte.NULL + Byte.LF + "*$"))) !== -1) { r.frames.push(unmarshallSingle(last_frame)); } else { r.partial = last_frame; } return r; }; Frame.marshall = function(command, headers, body) { var frame; frame = new Frame(command, headers, body); return frame.toString() + Byte.NULL; }; return Frame; })(); Client = (function() { var now; function Client(ws) { this.ws = ws; this.ws.binaryType = "arraybuffer"; this.counter = 0; this.connected = false; this.heartbeat = { outgoing: 10000, incoming: 10000 }; this.maxWebSocketFrameSize = 16 * 1024; this.subscriptions = {}; this.partialData = ''; } Client.prototype.debug = function(message) { var _ref; return typeof window !== "undefined" && window !== null ? (_ref = window.console) != null ? _ref.log(message) : void 0 : void 0; }; now = function() { if (Date.now) { return Date.now(); } else { return new Date().valueOf; } }; Client.prototype._transmit = function(command, headers, body) { var out; out = Frame.marshall(command, headers, body); if (typeof this.debug === "function") { this.debug(">>> " + out); } while (true) { if (out.length > this.maxWebSocketFrameSize) { this.ws.send(out.substring(0, this.maxWebSocketFrameSize)); out = out.substring(this.maxWebSocketFrameSize); if (typeof this.debug === "function") { this.debug("remaining = " + out.length); } } else { return this.ws.send(out); } } }; Client.prototype._setupHeartbeat = function(headers) { var serverIncoming, serverOutgoing, ttl, v, _ref, _ref1; if ((_ref = headers.version) !== Stomp.VERSIONS.V1_1 && _ref !== Stomp.VERSIONS.V1_2) { return; } _ref1 = (function() { var _i, _len, _ref1, _results; _ref1 = headers['heart-beat'].split(","); _results = []; for (_i = 0, _len = _ref1.length; _i < _len; _i++) { v = _ref1[_i]; _results.push(parseInt(v)); } return _results; })(), serverOutgoing = _ref1[0], serverIncoming = _ref1[1]; if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) { ttl = Math.max(this.heartbeat.outgoing, serverIncoming); if (typeof this.debug === "function") { this.debug("send PING every " + ttl + "ms"); } this.pinger = Stomp.setInterval(ttl, (function(_this) { return function() { _this.ws.send(Byte.LF); return typeof _this.debug === "function" ? _this.debug(">>> PING") : void 0; }; })(this)); } if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) { ttl = Math.max(this.heartbeat.incoming, serverOutgoing); if (typeof this.debug === "function") { this.debug("check PONG every " + ttl + "ms"); } return this.ponger = Stomp.setInterval(ttl, (function(_this) { return function() { var delta; delta = now() - _this.serverActivity; if (delta > ttl * 2) { if (typeof _this.debug === "function") { _this.debug("did not receive server activity for the last " + delta + "ms"); } return _this.ws.close(); } }; })(this)); } }; Client.prototype._parseConnect = function() { var args, connectCallback, errorCallback, headers; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; headers = {}; switch (args.length) { case 2: headers = args[0], connectCallback = args[1]; break; case 3: if (args[1] instanceof Function) { headers = args[0], connectCallback = args[1], errorCallback = args[2]; } else { headers.login = args[0], headers.passcode = args[1], connectCallback = args[2]; } break; case 4: headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3]; break; default: headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3], headers.host = args[4]; } return [headers, connectCallback, errorCallback]; }; Client.prototype.connect = function() { var args, errorCallback, headers, out; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; out = this._parseConnect.apply(this, args); headers = out[0], this.connectCallback = out[1], errorCallback = out[2]; if (typeof this.debug === "function") { this.debug("Opening Web Socket..."); } this.ws.onmessage = (function(_this) { return function(evt) { var arr, c, client, data, frame, messageID, onreceive, subscription, unmarshalledData, _i, _len, _ref, _results; data = typeof ArrayBuffer !== 'undefined' && evt.data instanceof ArrayBuffer ? (arr = new Uint8Array(evt.data), typeof _this.debug === "function" ? _this.debug("--- got data length: " + arr.length) : void 0, ((function() { var _i, _len, _results; _results = []; for (_i = 0, _len = arr.length; _i < _len; _i++) { c = arr[_i]; _results.push(String.fromCharCode(c)); } return _results; })()).join('')) : evt.data; _this.serverActivity = now(); if (data === Byte.LF) { if (typeof _this.debug === "function") { _this.debug("<<< PONG"); } return; } if (typeof _this.debug === "function") { _this.debug("<<< " + data); } unmarshalledData = Frame.unmarshall(_this.partialData + data); _this.partialData = unmarshalledData.partial; _ref = unmarshalledData.frames; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { frame = _ref[_i]; switch (frame.command) { case "CONNECTED": if (typeof _this.debug === "function") { _this.debug("connected to server " + frame.headers.server); } _this.connected = true; _this._setupHeartbeat(frame.headers); _results.push(typeof _this.connectCallback === "function" ? _this.connectCallback(frame) : void 0); break; case "MESSAGE": subscription = frame.headers.subscription; onreceive = _this.subscriptions[subscription] || _this.onreceive; if (onreceive) { client = _this; messageID = frame.headers["message-id"]; frame.ack = function(headers) { if (headers == null) { headers = {}; } return client.ack(messageID, subscription, headers); }; frame.nack = function(headers) { if (headers == null) { headers = {}; } return client.nack(messageID, subscription, headers); }; _results.push(onreceive(frame)); } else { _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled received MESSAGE: " + frame) : void 0); } break; case "RECEIPT": _results.push(typeof _this.onreceipt === "function" ? _this.onreceipt(frame) : void 0); break; case "ERROR": _results.push(typeof errorCallback === "function" ? errorCallback(frame) : void 0); break; default: _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled frame: " + frame) : void 0); } } return _results; }; })(this); this.ws.onclose = (function(_this) { return function() { var msg; msg = "Whoops! Lost connection to " + _this.ws.url; if (typeof _this.debug === "function") { _this.debug(msg); } _this._cleanUp(); return typeof errorCallback === "function" ? errorCallback(msg) : void 0; }; })(this); return this.ws.onopen = (function(_this) { return function() { if (typeof _this.debug === "function") { _this.debug('Web Socket Opened...'); } headers["accept-version"] = Stomp.VERSIONS.supportedVersions(); headers["heart-beat"] = [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(','); return _this._transmit("CONNECT", headers); }; })(this); }; Client.prototype.disconnect = function(disconnectCallback, headers) { if (headers == null) { headers = {}; } this._transmit("DISCONNECT", headers); this.ws.onclose = null; this.ws.close(); this._cleanUp(); return typeof disconnectCallback === "function" ? disconnectCallback() : void 0; }; Client.prototype._cleanUp = function() { this.connected = false; if (this.pinger) { Stomp.clearInterval(this.pinger); } if (this.ponger) { return Stomp.clearInterval(this.ponger); } }; Client.prototype.send = function(destination, headers, body) { if (headers == null) { headers = {}; } if (body == null) { body = ''; } headers.destination = destination; return this._transmit("SEND", headers, body); }; Client.prototype.subscribe = function(destination, callback, headers) { var client; if (headers == null) { headers = {}; } if (!headers.id) { headers.id = "sub-" + this.counter++; } headers.destination = destination; this.subscriptions[headers.id] = callback; this._transmit("SUBSCRIBE", headers); client = this; return { id: headers.id, unsubscribe: function() { return client.unsubscribe(headers.id); } }; }; Client.prototype.unsubscribe = function(id) { delete this.subscriptions[id]; return this._transmit("UNSUBSCRIBE", { id: id }); }; Client.prototype.begin = function(transaction) { var client, txid; txid = transaction || "tx-" + this.counter++; this._transmit("BEGIN", { transaction: txid }); client = this; return { id: txid, commit: function() { return client.commit(txid); }, abort: function() { return client.abort(txid); } }; }; Client.prototype.commit = function(transaction) { return this._transmit("COMMIT", { transaction: transaction }); }; Client.prototype.abort = function(transaction) { return this._transmit("ABORT", { transaction: transaction }); }; Client.prototype.ack = function(messageID, subscription, headers) { if (headers == null) { headers = {}; } headers["message-id"] = messageID; headers.subscription = subscription; return this._transmit("ACK", headers); }; Client.prototype.nack = function(messageID, subscription, headers) { if (headers == null) { headers = {}; } headers["message-id"] = messageID; headers.subscription = subscription; return this._transmit("NACK", headers); }; return Client; })(); Stomp = { VERSIONS: { V1_0: '1.0', V1_1: '1.1', V1_2: '1.2', supportedVersions: function() { return '1.1,1.0'; } }, client: function(url, protocols) { var klass, ws; if (protocols == null) { protocols = ['v10.stomp', 'v11.stomp']; } klass = Stomp.WebSocketClass || WebSocket; ws = new klass(url, protocols); return new Client(ws); }, over: function(ws) { return new Client(ws); }, Frame: Frame }; if (typeof exports !== "undefined" && exports !== null) { exports.Stomp = Stomp; } if (typeof window !== "undefined" && window !== null) { Stomp.setInterval = function(interval, f) { return window.setInterval(f, interval); }; Stomp.clearInterval = function(id) { return window.clearInterval(id); }; window.Stomp = Stomp; } else if (!exports) { self.Stomp = Stomp; } }).call(this); ================================================ FILE: demo-websocket/src/main/resources/static/server.html ================================================ 服务器信息
    手动连接 断开连接
    CPU信息
    内存信息
    服务器信息
    Java虚拟机信息
    磁盘状态
    ================================================ FILE: demo-websocket/src/test/java/com/xkcoding/websocket/SpringBootDemoWebsocketApplicationTests.java ================================================ package com.xkcoding.websocket; 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 SpringBootDemoWebsocketApplicationTests { @Test public void contextLoads() { } } ================================================ FILE: demo-websocket-socketio/.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-websocket-socketio/README.md ================================================ # spring-boot-demo-websocket-socketio > 此 demo 主要演示了 Spring Boot 如何使用 `netty-socketio` 集成 WebSocket,实现一个简单的聊天室。 ## 1. 代码 ### 1.1. pom.xml ```xml 4.0.0 spring-boot-demo-websocket-socketio 1.0.0-SNAPSHOT jar spring-boot-demo-websocket-socketio Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.7.16 com.corundumstudio.socketio netty-socketio ${netty-socketio.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all org.projectlombok lombok true spring-boot-demo-websocket-socketio org.springframework.boot spring-boot-maven-plugin ``` ### 1.2. ServerConfig.java > websocket服务器配置,包括服务器IP、端口信息、以及连接认证等配置 ```java /** *

    * websocket服务器配置 *

    * * @author yangkai.shen * @date Created in 2018-12-18 16:42 */ @Configuration @EnableConfigurationProperties({WsConfig.class}) public class ServerConfig { @Bean public SocketIOServer server(WsConfig wsConfig) { com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration(); config.setHostname(wsConfig.getHost()); config.setPort(wsConfig.getPort()); //这个listener可以用来进行身份验证 config.setAuthorizationListener(data -> { // http://localhost:8081?token=xxxxxxx // 例如果使用上面的链接进行connect,可以使用如下代码获取用户密码信息,本文不做身份验证 String token = data.getSingleUrlParam("token"); // 校验token的合法性,实际业务需要校验token是否过期等等,参考 spring-boot-demo-rbac-security 里的 JwtUtil // 如果认证不通过会返回一个 Socket.EVENT_CONNECT_ERROR 事件 return StrUtil.isNotBlank(token); }); return new SocketIOServer(config); } /** * Spring 扫描自定义注解 */ @Bean public SpringAnnotationScanner springAnnotationScanner(SocketIOServer server) { return new SpringAnnotationScanner(server); } } ``` ### 1.3. MessageEventHandler.java > 核心事件处理类,主要处理客户端发起的消息事件,以及主动往客户端发起事件 ```java /** *

    * 消息事件处理 *

    * * @author yangkai.shen * @date Created in 2018-12-18 18:57 */ @Component @Slf4j public class MessageEventHandler { @Autowired private SocketIOServer server; @Autowired private DbTemplate dbTemplate; /** * 添加connect事件,当客户端发起连接时调用 * * @param client 客户端对象 */ @OnConnect public void onConnect(SocketIOClient client) { if (client != null) { String token = client.getHandshakeData().getSingleUrlParam("token"); // 模拟用户id 和token一致 String userId = client.getHandshakeData().getSingleUrlParam("token"); UUID sessionId = client.getSessionId(); dbTemplate.save(userId, sessionId); log.info("连接成功,【token】= {},【sessionId】= {}", token, sessionId); } else { log.error("客户端为空"); } } /** * 添加disconnect事件,客户端断开连接时调用,刷新客户端信息 * * @param client 客户端对象 */ @OnDisconnect public void onDisconnect(SocketIOClient client) { if (client != null) { String token = client.getHandshakeData().getSingleUrlParam("token"); // 模拟用户id 和token一致 String userId = client.getHandshakeData().getSingleUrlParam("token"); UUID sessionId = client.getSessionId(); dbTemplate.deleteByUserId(userId); log.info("客户端断开连接,【token】= {},【sessionId】= {}", token, sessionId); client.disconnect(); } else { log.error("客户端为空"); } } /** * 加入群聊 * * @param client 客户端 * @param request 请求 * @param data 群聊 */ @OnEvent(value = Event.JOIN) public void onJoinEvent(SocketIOClient client, AckRequest request, JoinRequest data) { log.info("用户:{} 已加入群聊:{}", data.getUserId(), data.getGroupId()); client.joinRoom(data.getGroupId()); server.getRoomOperations(data.getGroupId()).sendEvent(Event.JOIN, data); } @OnEvent(value = Event.CHAT) public void onChatEvent(SocketIOClient client, AckRequest request, SingleMessageRequest data) { Optional toUser = dbTemplate.findByUserId(data.getToUid()); if (toUser.isPresent()) { log.info("用户 {} 刚刚私信了用户 {}:{}", data.getFromUid(), data.getToUid(), data.getMessage()); sendToSingle(toUser.get(), data); client.sendEvent(Event.CHAT_RECEIVED, "发送成功"); } else { client.sendEvent(Event.CHAT_REFUSED, "发送失败,对方不想理你"); } } @OnEvent(value = Event.GROUP) public void onGroupEvent(SocketIOClient client, AckRequest request, GroupMessageRequest data) { Collection clients = server.getRoomOperations(data.getGroupId()).getClients(); boolean inGroup = false; for (SocketIOClient socketIOClient : clients) { if (ObjectUtil.equal(socketIOClient.getSessionId(), client.getSessionId())) { inGroup = true; break; } } if (inGroup) { log.info("群号 {} 收到来自 {} 的群聊消息:{}", data.getGroupId(), data.getFromUid(), data.getMessage()); sendToGroup(data); } else { request.sendAckData("请先加群!"); } } /** * 单聊 */ public void sendToSingle(UUID sessionId, SingleMessageRequest message) { server.getClient(sessionId).sendEvent(Event.CHAT, message); } /** * 广播 */ public void sendToBroadcast(BroadcastMessageRequest message) { log.info("系统紧急广播一条通知:{}", message.getMessage()); for (UUID clientId : dbTemplate.findAll()) { if (server.getClient(clientId) == null) { continue; } server.getClient(clientId).sendEvent(Event.BROADCAST, message); } } /** * 群聊 */ public void sendToGroup(GroupMessageRequest message) { server.getRoomOperations(message.getGroupId()).sendEvent(Event.GROUP, message); } } ``` ### 1.4. ServerRunner.java > websocket 服务器启动类 ```java /** *

    * websocket服务器启动 *

    * * @author yangkai.shen * @date Created in 2018-12-18 17:07 */ @Component @Slf4j public class ServerRunner implements CommandLineRunner { @Autowired private SocketIOServer server; @Override public void run(String... args) { server.start(); log.info("websocket 服务器启动成功。。。"); } } ``` ## 2. 运行方式 1. 启动 `SpringBootDemoWebsocketSocketioApplication.java` 2. 使用不同的浏览器,访问 http://localhost:8080/demo/index.html ## 3. 运行效果 **浏览器1:**![image-20181219152318079](http://static.xkcoding.com/spring-boot-demo/websocket/socketio/064155.jpg) **浏览器2:**![image-20181219152330156](http://static.xkcoding.com/spring-boot-demo/websocket/socketio/064154.jpg) ## 4. 参考 ### 4.1. 后端 1. Netty-socketio 官方仓库:https://github.com/mrniko/netty-socketio 2. SpringBoot系列 - 集成SocketIO实时通信:https://www.xncoding.com/2017/07/16/spring/sb-socketio.html 3. Spring Boot 集成 socket.io 后端实现消息实时通信:http://alexpdh.com/2017/09/03/springboot-socketio/ 4. Spring Boot实战之netty-socketio实现简单聊天室:http://blog.csdn.net/sun_t89/article/details/52060946 ### 4.2. 前端 1. socket.io 官网:https://socket.io/ 2. axios.js 用法:https://github.com/axios/axios#example ================================================ FILE: demo-websocket-socketio/pom.xml ================================================ 4.0.0 demo-websocket-socketio 1.0.0-SNAPSHOT jar demo-websocket-socketio Demo project for Spring Boot com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT UTF-8 UTF-8 1.8 1.7.16 com.corundumstudio.socketio netty-socketio ${netty-socketio.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all org.projectlombok lombok true demo-websocket-socketio org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/SpringBootDemoWebsocketSocketioApplication.java ================================================ package com.xkcoding.websocket.socketio; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-12-12 13:59 */ @SpringBootApplication public class SpringBootDemoWebsocketSocketioApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoWebsocketSocketioApplication.class, args); } } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/config/DbTemplate.java ================================================ package com.xkcoding.websocket.socketio.config; import cn.hutool.core.collection.CollUtil; import org.springframework.stereotype.Component; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** *

    * 模拟数据库 *

    * * @author yangkai.shen * @date Created in 2018-12-18 19:12 */ @Component public class DbTemplate { /** * 模拟数据库存储 user_id <-> session_id 的关系 */ public static final ConcurrentHashMap DB = new ConcurrentHashMap<>(); /** * 获取所有SessionId * * @return SessionId列表 */ public List findAll() { return CollUtil.newArrayList(DB.values()); } /** * 根据UserId查询SessionId * * @param userId 用户id * @return SessionId */ public Optional findByUserId(String userId) { return Optional.ofNullable(DB.get(userId)); } /** * 保存/更新 user_id <-> session_id 的关系 * * @param userId 用户id * @param sessionId SessionId */ public void save(String userId, UUID sessionId) { DB.put(userId, sessionId); } /** * 删除 user_id <-> session_id 的关系 * * @param userId 用户id */ public void deleteByUserId(String userId) { DB.remove(userId); } } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/config/Event.java ================================================ package com.xkcoding.websocket.socketio.config; /** *

    * 事件常量 *

    * * @author yangkai.shen * @date Created in 2018-12-18 19:36 */ public interface Event { /** * 聊天事件 */ String CHAT = "chat"; /** * 广播消息 */ String BROADCAST = "broadcast"; /** * 群聊 */ String GROUP = "group"; /** * 加入群聊 */ String JOIN = "join"; } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/config/ServerConfig.java ================================================ package com.xkcoding.websocket.socketio.config; import cn.hutool.core.util.StrUtil; import com.corundumstudio.socketio.SocketIOServer; import com.corundumstudio.socketio.annotation.SpringAnnotationScanner; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** *

    * websocket服务器配置 *

    * * @author yangkai.shen * @date Created in 2018-12-18 16:42 */ @Configuration @EnableConfigurationProperties({WsConfig.class}) public class ServerConfig { @Bean public SocketIOServer server(WsConfig wsConfig) { com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration(); config.setHostname(wsConfig.getHost()); config.setPort(wsConfig.getPort()); //这个listener可以用来进行身份验证 config.setAuthorizationListener(data -> { // http://localhost:8081?token=xxxxxxx // 例如果使用上面的链接进行connect,可以使用如下代码获取用户密码信息,本文不做身份验证 String token = data.getSingleUrlParam("token"); // 校验token的合法性,实际业务需要校验token是否过期等等,参考 spring-boot-demo-rbac-security 里的 JwtUtil // 如果认证不通过会返回一个 Socket.EVENT_CONNECT_ERROR 事件 return StrUtil.isNotBlank(token); }); return new SocketIOServer(config); } /** * Spring 扫描自定义注解 */ @Bean public SpringAnnotationScanner springAnnotationScanner(SocketIOServer server) { return new SpringAnnotationScanner(server); } } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/config/WsConfig.java ================================================ package com.xkcoding.websocket.socketio.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** *

    * WebSocket配置类 *

    * * @author yangkai.shen * @date Created in 2018-12-18 16:41 */ @ConfigurationProperties(prefix = "ws.server") @Data public class WsConfig { /** * 端口号 */ private Integer port; /** * host */ private String host; } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/controller/MessageController.java ================================================ package com.xkcoding.websocket.socketio.controller; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import com.xkcoding.websocket.socketio.handler.MessageEventHandler; import com.xkcoding.websocket.socketio.payload.BroadcastMessageRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.lang.reflect.Field; /** *

    * 消息发送Controller *

    * * @author yangkai.shen * @date Created in 2018-12-18 19:50 */ @RestController @RequestMapping("/send") @Slf4j public class MessageController { @Autowired private MessageEventHandler messageHandler; @PostMapping("/broadcast") public Dict broadcast(@RequestBody BroadcastMessageRequest message) { if (isBlank(message)) { return Dict.create().set("flag", false).set("code", 400).set("message", "参数为空"); } messageHandler.sendToBroadcast(message); return Dict.create().set("flag", true).set("code", 200).set("message", "发送成功"); } /** * 判断Bean是否为空对象或者空白字符串,空对象表示本身为null或者所有属性都为null * * @param bean Bean对象 * @return 是否为空,true - 空 / false - 非空 */ private boolean isBlank(Object bean) { if (null != bean) { for (Field field : ReflectUtil.getFields(bean.getClass())) { Object fieldValue = ReflectUtil.getFieldValue(bean, field); if (null != fieldValue) { if (fieldValue instanceof String && StrUtil.isNotBlank((String) fieldValue)) { return false; } else if (!(fieldValue instanceof String)) { return false; } } } } return true; } } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/handler/MessageEventHandler.java ================================================ package com.xkcoding.websocket.socketio.handler; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ObjectUtil; import com.corundumstudio.socketio.AckRequest; import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.SocketIOServer; import com.corundumstudio.socketio.annotation.OnConnect; import com.corundumstudio.socketio.annotation.OnDisconnect; import com.corundumstudio.socketio.annotation.OnEvent; import com.xkcoding.websocket.socketio.config.DbTemplate; import com.xkcoding.websocket.socketio.config.Event; import com.xkcoding.websocket.socketio.payload.BroadcastMessageRequest; import com.xkcoding.websocket.socketio.payload.GroupMessageRequest; import com.xkcoding.websocket.socketio.payload.JoinRequest; import com.xkcoding.websocket.socketio.payload.SingleMessageRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Optional; import java.util.UUID; /** *

    * 消息事件处理 *

    * * @author yangkai.shen * @date Created in 2018-12-18 18:57 */ @Component @Slf4j public class MessageEventHandler { @Autowired private SocketIOServer server; @Autowired private DbTemplate dbTemplate; /** * 添加connect事件,当客户端发起连接时调用 * * @param client 客户端对象 */ @OnConnect public void onConnect(SocketIOClient client) { if (client != null) { String token = client.getHandshakeData().getSingleUrlParam("token"); // 模拟用户id 和token一致 String userId = client.getHandshakeData().getSingleUrlParam("token"); UUID sessionId = client.getSessionId(); dbTemplate.save(userId, sessionId); log.info("连接成功,【token】= {},【sessionId】= {}", token, sessionId); } else { log.error("客户端为空"); } } /** * 添加disconnect事件,客户端断开连接时调用,刷新客户端信息 * * @param client 客户端对象 */ @OnDisconnect public void onDisconnect(SocketIOClient client) { if (client != null) { String token = client.getHandshakeData().getSingleUrlParam("token"); // 模拟用户id 和token一致 String userId = client.getHandshakeData().getSingleUrlParam("token"); UUID sessionId = client.getSessionId(); dbTemplate.deleteByUserId(userId); log.info("客户端断开连接,【token】= {},【sessionId】= {}", token, sessionId); client.disconnect(); } else { log.error("客户端为空"); } } /** * 加入群聊 * * @param client 客户端 * @param request 请求 * @param data 群聊 */ @OnEvent(value = Event.JOIN) public void onJoinEvent(SocketIOClient client, AckRequest request, JoinRequest data) { log.info("用户:{} 已加入群聊:{}", data.getUserId(), data.getGroupId()); client.joinRoom(data.getGroupId()); server.getRoomOperations(data.getGroupId()).sendEvent(Event.JOIN, data); } @OnEvent(value = Event.CHAT) public void onChatEvent(SocketIOClient client, AckRequest request, SingleMessageRequest data) { Optional toUser = dbTemplate.findByUserId(data.getToUid()); if (toUser.isPresent()) { log.info("用户 {} 刚刚私信了用户 {}:{}", data.getFromUid(), data.getToUid(), data.getMessage()); sendToSingle(toUser.get(), data); request.sendAckData(Dict.create().set("flag", true).set("message", "发送成功")); } else { request.sendAckData(Dict.create().set("flag", false).set("message", "发送失败,对方不想理你(" + data.getToUid() + "不在线)")); } } @OnEvent(value = Event.GROUP) public void onGroupEvent(SocketIOClient client, AckRequest request, GroupMessageRequest data) { Collection clients = server.getRoomOperations(data.getGroupId()).getClients(); boolean inGroup = false; for (SocketIOClient socketIOClient : clients) { if (ObjectUtil.equal(socketIOClient.getSessionId(), client.getSessionId())) { inGroup = true; break; } } if (inGroup) { log.info("群号 {} 收到来自 {} 的群聊消息:{}", data.getGroupId(), data.getFromUid(), data.getMessage()); sendToGroup(data); } else { request.sendAckData("请先加群!"); } } /** * 单聊 */ public void sendToSingle(UUID sessionId, SingleMessageRequest message) { server.getClient(sessionId).sendEvent(Event.CHAT, message); } /** * 广播 */ public void sendToBroadcast(BroadcastMessageRequest message) { log.info("系统紧急广播一条通知:{}", message.getMessage()); for (UUID clientId : dbTemplate.findAll()) { if (server.getClient(clientId) == null) { continue; } server.getClient(clientId).sendEvent(Event.BROADCAST, message); } } /** * 群聊 */ public void sendToGroup(GroupMessageRequest message) { server.getRoomOperations(message.getGroupId()).sendEvent(Event.GROUP, message); } } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/init/ServerRunner.java ================================================ package com.xkcoding.websocket.socketio.init; import com.corundumstudio.socketio.SocketIOServer; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; /** *

    * websocket服务器启动 *

    * * @author yangkai.shen * @date Created in 2018-12-18 17:07 */ @Component @Slf4j public class ServerRunner implements CommandLineRunner { @Autowired private SocketIOServer server; @Override public void run(String... args) { server.start(); log.info("websocket 服务器启动成功。。。"); } } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/payload/BroadcastMessageRequest.java ================================================ package com.xkcoding.websocket.socketio.payload; import lombok.Data; /** *

    * 广播消息载荷 *

    * * @author yangkai.shen * @date Created in 2018-12-18 20:01 */ @Data public class BroadcastMessageRequest { /** * 消息内容 */ private String message; } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/payload/GroupMessageRequest.java ================================================ package com.xkcoding.websocket.socketio.payload; import lombok.Data; /** *

    * 群聊消息载荷 *

    * * @author yangkai.shen * @date Created in 2018-12-18 16:59 */ @Data public class GroupMessageRequest { /** * 消息发送方用户id */ private String fromUid; /** * 群组id */ private String groupId; /** * 消息内容 */ private String message; } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/payload/JoinRequest.java ================================================ package com.xkcoding.websocket.socketio.payload; import lombok.Data; /** *

    * 加群载荷 *

    * * @author yangkai.shen * @date Created in 2018-12-19 13:36 */ @Data public class JoinRequest { /** * 用户id */ private String userId; /** * 群名称 */ private String groupId; } ================================================ FILE: demo-websocket-socketio/src/main/java/com/xkcoding/websocket/socketio/payload/SingleMessageRequest.java ================================================ package com.xkcoding.websocket.socketio.payload; import lombok.Data; /** *

    * 私聊消息载荷 *

    * * @author yangkai.shen * @date Created in 2018-12-18 17:02 */ @Data public class SingleMessageRequest { /** * 消息发送方用户id */ private String fromUid; /** * 消息接收方用户id */ private String toUid; /** * 消息内容 */ private String message; } ================================================ FILE: demo-websocket-socketio/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo ws: server: port: 8081 host: localhost ================================================ FILE: demo-websocket-socketio/src/main/resources/static/bootstrap.css ================================================ /*! * Bootstrap v2.0.4 * * Copyright 2012 Twitter, Inc * Licensed under the Apache License v2.0 * http://www.apache.org/licenses/LICENSE-2.0 * * Designed and built with all the love in the world @twitter by @mdo and @fat. */ article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } audio:not([controls]) { display: none; } html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } a:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } a:hover, a:active { outline: 0; } sub, sup { position: relative; font-size: 75%; line-height: 0; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } img { max-width: 100%; vertical-align: middle; border: 0; -ms-interpolation-mode: bicubic; } #map_canvas img { max-width: none; } button, input, select, textarea { margin: 0; font-size: 100%; vertical-align: middle; } button, input { *overflow: visible; line-height: normal; } button::-moz-focus-inner, input::-moz-focus-inner { padding: 0; border: 0; } button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; } input[type="search"] { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; -webkit-appearance: textfield; } input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } textarea { overflow: auto; vertical-align: top; } .clearfix { *zoom: 1; } .clearfix:before, .clearfix:after { display: table; content: ""; } .clearfix:after { clear: both; } .hide-text { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .input-block-level { display: block; width: 100%; min-height: 28px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; } body { margin: 0; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; line-height: 18px; color: #333333; background-color: #ffffff; } a { color: #0088cc; text-decoration: none; } a:hover { color: #005580; text-decoration: underline; } .row { margin-left: -20px; *zoom: 1; } .row:before, .row:after { display: table; content: ""; } .row:after { clear: both; } [class*="span"] { float: left; margin-left: 20px; } .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { width: 940px; } .span12 { width: 940px; } .span11 { width: 860px; } .span10 { width: 780px; } .span9 { width: 700px; } .span8 { width: 620px; } .span7 { width: 540px; } .span6 { width: 460px; } .span5 { width: 380px; } .span4 { width: 300px; } .span3 { width: 220px; } .span2 { width: 140px; } .span1 { width: 60px; } .offset12 { margin-left: 980px; } .offset11 { margin-left: 900px; } .offset10 { margin-left: 820px; } .offset9 { margin-left: 740px; } .offset8 { margin-left: 660px; } .offset7 { margin-left: 580px; } .offset6 { margin-left: 500px; } .offset5 { margin-left: 420px; } .offset4 { margin-left: 340px; } .offset3 { margin-left: 260px; } .offset2 { margin-left: 180px; } .offset1 { margin-left: 100px; } .row-fluid { width: 100%; *zoom: 1; } .row-fluid:before, .row-fluid:after { display: table; content: ""; } .row-fluid:after { clear: both; } .row-fluid [class*="span"] { display: block; float: left; width: 100%; min-height: 28px; margin-left: 2.127659574%; *margin-left: 2.0744680846382977%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; } .row-fluid [class*="span"]:first-child { margin-left: 0; } .row-fluid .span12 { width: 99.99999998999999%; *width: 99.94680850063828%; } .row-fluid .span11 { width: 91.489361693%; *width: 91.4361702036383%; } .row-fluid .span10 { width: 82.97872339599999%; *width: 82.92553190663828%; } .row-fluid .span9 { width: 74.468085099%; *width: 74.4148936096383%; } .row-fluid .span8 { width: 65.95744680199999%; *width: 65.90425531263828%; } .row-fluid .span7 { width: 57.446808505%; *width: 57.3936170156383%; } .row-fluid .span6 { width: 48.93617020799999%; *width: 48.88297871863829%; } .row-fluid .span5 { width: 40.425531911%; *width: 40.3723404216383%; } .row-fluid .span4 { width: 31.914893614%; *width: 31.8617021246383%; } .row-fluid .span3 { width: 23.404255317%; *width: 23.3510638276383%; } .row-fluid .span2 { width: 14.89361702%; *width: 14.8404255306383%; } .row-fluid .span1 { width: 6.382978723%; *width: 6.329787233638298%; } .container { margin-right: auto; margin-left: auto; *zoom: 1; } .container:before, .container:after { display: table; content: ""; } .container:after { clear: both; } .container-fluid { padding-right: 20px; padding-left: 20px; *zoom: 1; } .container-fluid:before, .container-fluid:after { display: table; content: ""; } .container-fluid:after { clear: both; } p { margin: 0 0 9px; } p small { font-size: 11px; color: #999999; } .lead { margin-bottom: 18px; font-size: 20px; font-weight: 200; line-height: 27px; } h1, h2, h3, h4, h5, h6 { margin: 0; font-family: inherit; font-weight: bold; color: inherit; text-rendering: optimizelegibility; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-weight: normal; color: #999999; } h1 { font-size: 30px; line-height: 36px; } h1 small { font-size: 18px; } h2 { font-size: 24px; line-height: 36px; } h2 small { font-size: 18px; } h3 { font-size: 18px; line-height: 27px; } h3 small { font-size: 14px; } h4, h5, h6 { line-height: 18px; } h4 { font-size: 14px; } h4 small { font-size: 12px; } h5 { font-size: 12px; } h6 { font-size: 11px; color: #999999; text-transform: uppercase; } .page-header { padding-bottom: 17px; margin: 18px 0; border-bottom: 1px solid #eeeeee; } .page-header h1 { line-height: 1; } ul, ol { padding: 0; margin: 0 0 9px 25px; } ul ul, ul ol, ol ol, ol ul { margin-bottom: 0; } ul { list-style: disc; } ol { list-style: decimal; } li { line-height: 18px; } ul.unstyled, ol.unstyled { margin-left: 0; list-style: none; } dl { margin-bottom: 18px; } dt, dd { line-height: 18px; } dt { font-weight: bold; line-height: 17px; } dd { margin-left: 9px; } .dl-horizontal dt { float: left; width: 120px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } .dl-horizontal dd { margin-left: 130px; } hr { margin: 18px 0; border: 0; border-top: 1px solid #eeeeee; border-bottom: 1px solid #ffffff; } strong { font-weight: bold; } em { font-style: italic; } .muted { color: #999999; } abbr[title] { cursor: help; border-bottom: 1px dotted #999999; } abbr.initialism { font-size: 90%; text-transform: uppercase; } blockquote { padding: 0 0 0 15px; margin: 0 0 18px; border-left: 5px solid #eeeeee; } blockquote p { margin-bottom: 0; font-size: 16px; font-weight: 300; line-height: 22.5px; } blockquote small { display: block; line-height: 18px; color: #999999; } blockquote small:before { content: '\2014 \00A0'; } blockquote.pull-right { float: right; padding-right: 15px; padding-left: 0; border-right: 5px solid #eeeeee; border-left: 0; } blockquote.pull-right p, blockquote.pull-right small { text-align: right; } q:before, q:after, blockquote:before, blockquote:after { content: ""; } address { display: block; margin-bottom: 18px; font-style: normal; line-height: 18px; } small { font-size: 100%; } cite { font-style: normal; } code, pre { padding: 0 3px 2px; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 12px; color: #333333; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } code { padding: 2px 4px; color: #d14; background-color: #f7f7f9; border: 1px solid #e1e1e8; } pre { display: block; padding: 8.5px; margin: 0 0 9px; font-size: 12.025px; line-height: 18px; word-break: break-all; word-wrap: break-word; white-space: pre; white-space: pre-wrap; background-color: #f5f5f5; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } pre.prettyprint { margin-bottom: 18px; } pre code { padding: 0; color: inherit; background-color: transparent; border: 0; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } form { margin: 0 0 18px; } fieldset { padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; padding: 0; margin-bottom: 27px; font-size: 19.5px; line-height: 36px; color: #333333; border: 0; border-bottom: 1px solid #e5e5e5; } legend small { font-size: 13.5px; color: #999999; } label, input, button, select, textarea { font-size: 13px; font-weight: normal; line-height: 18px; } input, button, select, textarea { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } label { display: block; margin-bottom: 5px; } select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input { display: inline-block; height: 18px; padding: 4px; margin-bottom: 9px; font-size: 13px; line-height: 18px; color: #555555; } input, textarea { width: 210px; } textarea { height: auto; } textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input { background-color: #ffffff; border: 1px solid #cccccc; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s; -ms-transition: border linear 0.2s, box-shadow linear 0.2s; -o-transition: border linear 0.2s, box-shadow linear 0.2s; transition: border linear 0.2s, box-shadow linear 0.2s; } textarea:focus, input[type="text"]:focus, input[type="password"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus, .uneditable-input:focus { border-color: rgba(82, 168, 236, 0.8); outline: 0; outline: thin dotted \9; /* IE6-9 */ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); } input[type="radio"], input[type="checkbox"] { margin: 3px 0; *margin-top: 0; /* IE7 */ line-height: normal; cursor: pointer; } input[type="submit"], input[type="reset"], input[type="button"], input[type="radio"], input[type="checkbox"] { width: auto; } .uneditable-textarea { width: auto; height: auto; } select, input[type="file"] { height: 28px; /* In IE7, the height of the select element cannot be changed by height, only font-size */ *margin-top: 4px; /* For IE7, add top margin to align select with labels */ line-height: 28px; } select { width: 220px; border: 1px solid #bbb; } select[multiple], select[size] { height: auto; } select:focus, input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .radio, .checkbox { min-height: 18px; padding-left: 18px; } .radio input[type="radio"], .checkbox input[type="checkbox"] { float: left; margin-left: -18px; } .controls > .radio:first-child, .controls > .checkbox:first-child { padding-top: 5px; } .radio.inline, .checkbox.inline { display: inline-block; padding-top: 5px; margin-bottom: 0; vertical-align: middle; } .radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline { margin-left: 10px; } .input-mini { width: 60px; } .input-small { width: 90px; } .input-medium { width: 150px; } .input-large { width: 210px; } .input-xlarge { width: 270px; } .input-xxlarge { width: 530px; } input[class*="span"], select[class*="span"], textarea[class*="span"], .uneditable-input[class*="span"], .row-fluid input[class*="span"], .row-fluid select[class*="span"], .row-fluid textarea[class*="span"], .row-fluid .uneditable-input[class*="span"] { float: none; margin-left: 0; } .input-append input[class*="span"], .input-append .uneditable-input[class*="span"], .input-prepend input[class*="span"], .input-prepend .uneditable-input[class*="span"], .row-fluid .input-prepend [class*="span"], .row-fluid .input-append [class*="span"] { display: inline-block; } input, textarea, .uneditable-input { margin-left: 0; } input.span12, textarea.span12, .uneditable-input.span12 { width: 930px; } input.span11, textarea.span11, .uneditable-input.span11 { width: 850px; } input.span10, textarea.span10, .uneditable-input.span10 { width: 770px; } input.span9, textarea.span9, .uneditable-input.span9 { width: 690px; } input.span8, textarea.span8, .uneditable-input.span8 { width: 610px; } input.span7, textarea.span7, .uneditable-input.span7 { width: 530px; } input.span6, textarea.span6, .uneditable-input.span6 { width: 450px; } input.span5, textarea.span5, .uneditable-input.span5 { width: 370px; } input.span4, textarea.span4, .uneditable-input.span4 { width: 290px; } input.span3, textarea.span3, .uneditable-input.span3 { width: 210px; } input.span2, textarea.span2, .uneditable-input.span2 { width: 130px; } input.span1, textarea.span1, .uneditable-input.span1 { width: 50px; } input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { cursor: not-allowed; background-color: #eeeeee; border-color: #ddd; } input[type="radio"][disabled], input[type="checkbox"][disabled], input[type="radio"][readonly], input[type="checkbox"][readonly] { background-color: transparent; } .control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline { color: #c09853; } .control-group.warning .checkbox, .control-group.warning .radio, .control-group.warning input, .control-group.warning select, .control-group.warning textarea { color: #c09853; border-color: #c09853; } .control-group.warning .checkbox:focus, .control-group.warning .radio:focus, .control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { border-color: #a47e3c; -webkit-box-shadow: 0 0 6px #dbc59e; -moz-box-shadow: 0 0 6px #dbc59e; box-shadow: 0 0 6px #dbc59e; } .control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on { color: #c09853; background-color: #fcf8e3; border-color: #c09853; } .control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline { color: #b94a48; } .control-group.error .checkbox, .control-group.error .radio, .control-group.error input, .control-group.error select, .control-group.error textarea { color: #b94a48; border-color: #b94a48; } .control-group.error .checkbox:focus, .control-group.error .radio:focus, .control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus { border-color: #953b39; -webkit-box-shadow: 0 0 6px #d59392; -moz-box-shadow: 0 0 6px #d59392; box-shadow: 0 0 6px #d59392; } .control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on { color: #b94a48; background-color: #f2dede; border-color: #b94a48; } .control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline { color: #468847; } .control-group.success .checkbox, .control-group.success .radio, .control-group.success input, .control-group.success select, .control-group.success textarea { color: #468847; border-color: #468847; } .control-group.success .checkbox:focus, .control-group.success .radio:focus, .control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus { border-color: #356635; -webkit-box-shadow: 0 0 6px #7aba7b; -moz-box-shadow: 0 0 6px #7aba7b; box-shadow: 0 0 6px #7aba7b; } .control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on { color: #468847; background-color: #dff0d8; border-color: #468847; } input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { color: #b94a48; border-color: #ee5f5b; } input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { border-color: #e9322d; -webkit-box-shadow: 0 0 6px #f8b9b7; -moz-box-shadow: 0 0 6px #f8b9b7; box-shadow: 0 0 6px #f8b9b7; } .form-actions { padding: 17px 20px 18px; margin-top: 18px; margin-bottom: 18px; background-color: #f5f5f5; border-top: 1px solid #e5e5e5; *zoom: 1; } .form-actions:before, .form-actions:after { display: table; content: ""; } .form-actions:after { clear: both; } .uneditable-input { overflow: hidden; white-space: nowrap; cursor: not-allowed; background-color: #ffffff; border-color: #eee; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); } :-moz-placeholder { color: #999999; } :-ms-input-placeholder { color: #999999; } ::-webkit-input-placeholder { color: #999999; } .help-block, .help-inline { color: #555555; } .help-block { display: block; margin-bottom: 9px; } .help-inline { display: inline-block; *display: inline; padding-left: 5px; vertical-align: middle; *zoom: 1; } .input-prepend, .input-append { margin-bottom: 5px; } .input-prepend input, .input-append input, .input-prepend select, .input-append select, .input-prepend .uneditable-input, .input-append .uneditable-input { position: relative; margin-bottom: 0; *margin-left: 0; vertical-align: middle; -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .input-prepend input:focus, .input-append input:focus, .input-prepend select:focus, .input-append select:focus, .input-prepend .uneditable-input:focus, .input-append .uneditable-input:focus { z-index: 2; } .input-prepend .uneditable-input, .input-append .uneditable-input { border-left-color: #ccc; } .input-prepend .add-on, .input-append .add-on { display: inline-block; width: auto; height: 18px; min-width: 16px; padding: 4px 5px; font-weight: normal; line-height: 18px; text-align: center; text-shadow: 0 1px 0 #ffffff; vertical-align: middle; background-color: #eeeeee; border: 1px solid #ccc; } .input-prepend .add-on, .input-append .add-on, .input-prepend .btn, .input-append .btn { margin-left: -1px; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .input-prepend .active, .input-append .active { background-color: #a9dba9; border-color: #46a546; } .input-prepend .add-on, .input-prepend .btn { margin-right: -1px; } .input-prepend .add-on:first-child, .input-prepend .btn:first-child { -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .input-append input, .input-append select, .input-append .uneditable-input { -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .input-append .uneditable-input { border-right-color: #ccc; border-left-color: #eee; } .input-append .add-on:last-child, .input-append .btn:last-child { -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .input-prepend.input-append input, .input-prepend.input-append select, .input-prepend.input-append .uneditable-input { -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .input-prepend.input-append .add-on:first-child, .input-prepend.input-append .btn:first-child { margin-right: -1px; -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .input-prepend.input-append .add-on:last-child, .input-prepend.input-append .btn:last-child { margin-left: -1px; -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .search-query { padding-right: 14px; padding-right: 4px \9; padding-left: 14px; padding-left: 4px \9; /* IE7-8 doesn't have border-radius, so don't indent the padding */ margin-bottom: 0; -webkit-border-radius: 14px; -moz-border-radius: 14px; border-radius: 14px; } .form-search input, .form-inline input, .form-horizontal input, .form-search textarea, .form-inline textarea, .form-horizontal textarea, .form-search select, .form-inline select, .form-horizontal select, .form-search .help-inline, .form-inline .help-inline, .form-horizontal .help-inline, .form-search .uneditable-input, .form-inline .uneditable-input, .form-horizontal .uneditable-input, .form-search .input-prepend, .form-inline .input-prepend, .form-horizontal .input-prepend, .form-search .input-append, .form-inline .input-append, .form-horizontal .input-append { display: inline-block; *display: inline; margin-bottom: 0; *zoom: 1; } .form-search .hide, .form-inline .hide, .form-horizontal .hide { display: none; } .form-search label, .form-inline label { display: inline-block; } .form-search .input-append, .form-inline .input-append, .form-search .input-prepend, .form-inline .input-prepend { margin-bottom: 0; } .form-search .radio, .form-search .checkbox, .form-inline .radio, .form-inline .checkbox { padding-left: 0; margin-bottom: 0; vertical-align: middle; } .form-search .radio input[type="radio"], .form-search .checkbox input[type="checkbox"], .form-inline .radio input[type="radio"], .form-inline .checkbox input[type="checkbox"] { float: left; margin-right: 3px; margin-left: 0; } .control-group { margin-bottom: 9px; } legend + .control-group { margin-top: 18px; -webkit-margin-top-collapse: separate; } .form-horizontal .control-group { margin-bottom: 18px; *zoom: 1; } .form-horizontal .control-group:before, .form-horizontal .control-group:after { display: table; content: ""; } .form-horizontal .control-group:after { clear: both; } .form-horizontal .control-label { float: left; width: 140px; padding-top: 5px; text-align: right; } .form-horizontal .controls { *display: inline-block; *padding-left: 20px; margin-left: 160px; *margin-left: 0; } .form-horizontal .controls:first-child { *padding-left: 160px; } .form-horizontal .help-block { margin-top: 9px; margin-bottom: 0; } .form-horizontal .form-actions { padding-left: 160px; } table { max-width: 100%; background-color: transparent; border-collapse: collapse; border-spacing: 0; } .table { width: 100%; margin-bottom: 18px; } .table th, .table td { padding: 8px; line-height: 18px; text-align: left; vertical-align: top; border-top: 1px solid #dddddd; } .table th { font-weight: bold; } .table thead th { vertical-align: bottom; } .table caption + thead tr:first-child th, .table caption + thead tr:first-child td, .table colgroup + thead tr:first-child th, .table colgroup + thead tr:first-child td, .table thead:first-child tr:first-child th, .table thead:first-child tr:first-child td { border-top: 0; } .table tbody + tbody { border-top: 2px solid #dddddd; } .table-condensed th, .table-condensed td { padding: 4px 5px; } .table-bordered { border: 1px solid #dddddd; border-collapse: separate; *border-collapse: collapsed; border-left: 0; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .table-bordered th, .table-bordered td { border-left: 1px solid #dddddd; } .table-bordered caption + thead tr:first-child th, .table-bordered caption + tbody tr:first-child th, .table-bordered caption + tbody tr:first-child td, .table-bordered colgroup + thead tr:first-child th, .table-bordered colgroup + tbody tr:first-child th, .table-bordered colgroup + tbody tr:first-child td, .table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { border-top: 0; } .table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-topleft: 4px; } .table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; -moz-border-radius-topright: 4px; } .table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { -webkit-border-radius: 0 0 0 4px; -moz-border-radius: 0 0 0 4px; border-radius: 0 0 0 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; -moz-border-radius-bottomleft: 4px; } .table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; -moz-border-radius-bottomright: 4px; } .table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { background-color: #f9f9f9; } .table tbody tr:hover td, .table tbody tr:hover th { background-color: #f5f5f5; } table .span1 { float: none; width: 44px; margin-left: 0; } table .span2 { float: none; width: 124px; margin-left: 0; } table .span3 { float: none; width: 204px; margin-left: 0; } table .span4 { float: none; width: 284px; margin-left: 0; } table .span5 { float: none; width: 364px; margin-left: 0; } table .span6 { float: none; width: 444px; margin-left: 0; } table .span7 { float: none; width: 524px; margin-left: 0; } table .span8 { float: none; width: 604px; margin-left: 0; } table .span9 { float: none; width: 684px; margin-left: 0; } table .span10 { float: none; width: 764px; margin-left: 0; } table .span11 { float: none; width: 844px; margin-left: 0; } table .span12 { float: none; width: 924px; margin-left: 0; } table .span13 { float: none; width: 1004px; margin-left: 0; } table .span14 { float: none; width: 1084px; margin-left: 0; } table .span15 { float: none; width: 1164px; margin-left: 0; } table .span16 { float: none; width: 1244px; margin-left: 0; } table .span17 { float: none; width: 1324px; margin-left: 0; } table .span18 { float: none; width: 1404px; margin-left: 0; } table .span19 { float: none; width: 1484px; margin-left: 0; } table .span20 { float: none; width: 1564px; margin-left: 0; } table .span21 { float: none; width: 1644px; margin-left: 0; } table .span22 { float: none; width: 1724px; margin-left: 0; } table .span23 { float: none; width: 1804px; margin-left: 0; } table .span24 { float: none; width: 1884px; margin-left: 0; } [class^="icon-"], [class*=" icon-"] { display: inline-block; width: 14px; height: 14px; *margin-right: .3em; line-height: 14px; vertical-align: text-top; background-image: url("../img/glyphicons-halflings.png"); background-position: 14px 14px; background-repeat: no-repeat; } [class^="icon-"]:last-child, [class*=" icon-"]:last-child { *margin-left: 0; } .icon-white { background-image: url("../img/glyphicons-halflings-white.png"); } .icon-glass { background-position: 0 0; } .icon-music { background-position: -24px 0; } .icon-search { background-position: -48px 0; } .icon-envelope { background-position: -72px 0; } .icon-heart { background-position: -96px 0; } .icon-star { background-position: -120px 0; } .icon-star-empty { background-position: -144px 0; } .icon-user { background-position: -168px 0; } .icon-film { background-position: -192px 0; } .icon-th-large { background-position: -216px 0; } .icon-th { background-position: -240px 0; } .icon-th-list { background-position: -264px 0; } .icon-ok { background-position: -288px 0; } .icon-remove { background-position: -312px 0; } .icon-zoom-in { background-position: -336px 0; } .icon-zoom-out { background-position: -360px 0; } .icon-off { background-position: -384px 0; } .icon-signal { background-position: -408px 0; } .icon-cog { background-position: -432px 0; } .icon-trash { background-position: -456px 0; } .icon-home { background-position: 0 -24px; } .icon-file { background-position: -24px -24px; } .icon-time { background-position: -48px -24px; } .icon-road { background-position: -72px -24px; } .icon-download-alt { background-position: -96px -24px; } .icon-download { background-position: -120px -24px; } .icon-upload { background-position: -144px -24px; } .icon-inbox { background-position: -168px -24px; } .icon-play-circle { background-position: -192px -24px; } .icon-repeat { background-position: -216px -24px; } .icon-refresh { background-position: -240px -24px; } .icon-list-alt { background-position: -264px -24px; } .icon-lock { background-position: -287px -24px; } .icon-flag { background-position: -312px -24px; } .icon-headphones { background-position: -336px -24px; } .icon-volume-off { background-position: -360px -24px; } .icon-volume-down { background-position: -384px -24px; } .icon-volume-up { background-position: -408px -24px; } .icon-qrcode { background-position: -432px -24px; } .icon-barcode { background-position: -456px -24px; } .icon-tag { background-position: 0 -48px; } .icon-tags { background-position: -25px -48px; } .icon-book { background-position: -48px -48px; } .icon-bookmark { background-position: -72px -48px; } .icon-print { background-position: -96px -48px; } .icon-camera { background-position: -120px -48px; } .icon-font { background-position: -144px -48px; } .icon-bold { background-position: -167px -48px; } .icon-italic { background-position: -192px -48px; } .icon-text-height { background-position: -216px -48px; } .icon-text-width { background-position: -240px -48px; } .icon-align-left { background-position: -264px -48px; } .icon-align-center { background-position: -288px -48px; } .icon-align-right { background-position: -312px -48px; } .icon-align-justify { background-position: -336px -48px; } .icon-list { background-position: -360px -48px; } .icon-indent-left { background-position: -384px -48px; } .icon-indent-right { background-position: -408px -48px; } .icon-facetime-video { background-position: -432px -48px; } .icon-picture { background-position: -456px -48px; } .icon-pencil { background-position: 0 -72px; } .icon-map-marker { background-position: -24px -72px; } .icon-adjust { background-position: -48px -72px; } .icon-tint { background-position: -72px -72px; } .icon-edit { background-position: -96px -72px; } .icon-share { background-position: -120px -72px; } .icon-check { background-position: -144px -72px; } .icon-move { background-position: -168px -72px; } .icon-step-backward { background-position: -192px -72px; } .icon-fast-backward { background-position: -216px -72px; } .icon-backward { background-position: -240px -72px; } .icon-play { background-position: -264px -72px; } .icon-pause { background-position: -288px -72px; } .icon-stop { background-position: -312px -72px; } .icon-forward { background-position: -336px -72px; } .icon-fast-forward { background-position: -360px -72px; } .icon-step-forward { background-position: -384px -72px; } .icon-eject { background-position: -408px -72px; } .icon-chevron-left { background-position: -432px -72px; } .icon-chevron-right { background-position: -456px -72px; } .icon-plus-sign { background-position: 0 -96px; } .icon-minus-sign { background-position: -24px -96px; } .icon-remove-sign { background-position: -48px -96px; } .icon-ok-sign { background-position: -72px -96px; } .icon-question-sign { background-position: -96px -96px; } .icon-info-sign { background-position: -120px -96px; } .icon-screenshot { background-position: -144px -96px; } .icon-remove-circle { background-position: -168px -96px; } .icon-ok-circle { background-position: -192px -96px; } .icon-ban-circle { background-position: -216px -96px; } .icon-arrow-left { background-position: -240px -96px; } .icon-arrow-right { background-position: -264px -96px; } .icon-arrow-up { background-position: -289px -96px; } .icon-arrow-down { background-position: -312px -96px; } .icon-share-alt { background-position: -336px -96px; } .icon-resize-full { background-position: -360px -96px; } .icon-resize-small { background-position: -384px -96px; } .icon-plus { background-position: -408px -96px; } .icon-minus { background-position: -433px -96px; } .icon-asterisk { background-position: -456px -96px; } .icon-exclamation-sign { background-position: 0 -120px; } .icon-gift { background-position: -24px -120px; } .icon-leaf { background-position: -48px -120px; } .icon-fire { background-position: -72px -120px; } .icon-eye-open { background-position: -96px -120px; } .icon-eye-close { background-position: -120px -120px; } .icon-warning-sign { background-position: -144px -120px; } .icon-plane { background-position: -168px -120px; } .icon-calendar { background-position: -192px -120px; } .icon-random { background-position: -216px -120px; } .icon-comment { background-position: -240px -120px; } .icon-magnet { background-position: -264px -120px; } .icon-chevron-up { background-position: -288px -120px; } .icon-chevron-down { background-position: -313px -119px; } .icon-retweet { background-position: -336px -120px; } .icon-shopping-cart { background-position: -360px -120px; } .icon-folder-close { background-position: -384px -120px; } .icon-folder-open { background-position: -408px -120px; } .icon-resize-vertical { background-position: -432px -119px; } .icon-resize-horizontal { background-position: -456px -118px; } .icon-hdd { background-position: 0 -144px; } .icon-bullhorn { background-position: -24px -144px; } .icon-bell { background-position: -48px -144px; } .icon-certificate { background-position: -72px -144px; } .icon-thumbs-up { background-position: -96px -144px; } .icon-thumbs-down { background-position: -120px -144px; } .icon-hand-right { background-position: -144px -144px; } .icon-hand-left { background-position: -168px -144px; } .icon-hand-up { background-position: -192px -144px; } .icon-hand-down { background-position: -216px -144px; } .icon-circle-arrow-right { background-position: -240px -144px; } .icon-circle-arrow-left { background-position: -264px -144px; } .icon-circle-arrow-up { background-position: -288px -144px; } .icon-circle-arrow-down { background-position: -312px -144px; } .icon-globe { background-position: -336px -144px; } .icon-wrench { background-position: -360px -144px; } .icon-tasks { background-position: -384px -144px; } .icon-filter { background-position: -408px -144px; } .icon-briefcase { background-position: -432px -144px; } .icon-fullscreen { background-position: -456px -144px; } .dropup, .dropdown { position: relative; } .dropdown-toggle { *margin-bottom: -3px; } .dropdown-toggle:active, .open .dropdown-toggle { outline: 0; } .caret { display: inline-block; width: 0; height: 0; vertical-align: top; border-top: 4px solid #000000; border-right: 4px solid transparent; border-left: 4px solid transparent; content: ""; opacity: 0.3; filter: alpha(opacity=30); } .dropdown .caret { margin-top: 8px; margin-left: 2px; } .dropdown:hover .caret, .open .caret { opacity: 1; filter: alpha(opacity=100); } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 160px; padding: 4px 0; margin: 1px 0 0; list-style: none; background-color: #ffffff; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); *border-right-width: 2px; *border-bottom-width: 2px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; } .dropdown-menu.pull-right { right: 0; left: auto; } .dropdown-menu .divider { *width: 100%; height: 1px; margin: 8px 1px; *margin: -5px 0 5px; overflow: hidden; background-color: #e5e5e5; border-bottom: 1px solid #ffffff; } .dropdown-menu a { display: block; padding: 3px 15px; clear: both; font-weight: normal; line-height: 18px; color: #333333; white-space: nowrap; } .dropdown-menu li > a:hover, .dropdown-menu .active > a, .dropdown-menu .active > a:hover { color: #ffffff; text-decoration: none; background-color: #0088cc; } .open { *z-index: 1000; } .open > .dropdown-menu { display: block; } .pull-right > .dropdown-menu { right: 0; left: auto; } .dropup .caret, .navbar-fixed-bottom .dropdown .caret { border-top: 0; border-bottom: 4px solid #000000; content: "\2191"; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; margin-bottom: 1px; } .typeahead { margin-top: 2px; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .well { min-height: 20px; padding: 19px; margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #eee; border: 1px solid rgba(0, 0, 0, 0.05); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); } .well blockquote { border-color: #ddd; border-color: rgba(0, 0, 0, 0.15); } .well-large { padding: 24px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; } .well-small { padding: 9px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .fade { opacity: 0; -webkit-transition: opacity 0.15s linear; -moz-transition: opacity 0.15s linear; -ms-transition: opacity 0.15s linear; -o-transition: opacity 0.15s linear; transition: opacity 0.15s linear; } .fade.in { opacity: 1; } .collapse { position: relative; height: 0; overflow: hidden; -webkit-transition: height 0.35s ease; -moz-transition: height 0.35s ease; -ms-transition: height 0.35s ease; -o-transition: height 0.35s ease; transition: height 0.35s ease; } .collapse.in { height: auto; } .close { float: right; font-size: 20px; font-weight: bold; line-height: 18px; color: #000000; text-shadow: 0 1px 0 #ffffff; opacity: 0.2; filter: alpha(opacity=20); } .close:hover { color: #000000; text-decoration: none; cursor: pointer; opacity: 0.4; filter: alpha(opacity=40); } button.close { padding: 0; cursor: pointer; background: transparent; border: 0; -webkit-appearance: none; } .btn { display: inline-block; *display: inline; padding: 4px 10px 4px; margin-bottom: 0; *margin-left: .3em; font-size: 13px; line-height: 18px; *line-height: 20px; color: #333333; text-align: center; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; cursor: pointer; background-color: #f5f5f5; *background-color: #e6e6e6; background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; border: 1px solid #cccccc; *border: 0; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border-color: #e6e6e6 #e6e6e6 #bfbfbf; border-bottom-color: #b3b3b3; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); *zoom: 1; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; *background-color: #d9d9d9; } .btn:active, .btn.active { background-color: #cccccc \9; } .btn:first-child { *margin-left: 0; } .btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; *background-color: #d9d9d9; /* Buttons in IE7 don't get borders, so darken on hover */ background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; } .btn:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .btn.active, .btn:active { background-color: #e6e6e6; background-color: #d9d9d9 \9; background-image: none; outline: 0; -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn.disabled, .btn[disabled] { cursor: default; background-color: #e6e6e6; background-image: none; opacity: 0.65; filter: alpha(opacity=65); -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } .btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .btn-large [class^="icon-"] { margin-top: 1px; } .btn-small { padding: 5px 9px; font-size: 11px; line-height: 16px; } .btn-small [class^="icon-"] { margin-top: -1px; } .btn-mini { padding: 2px 6px; font-size: 11px; line-height: 14px; } .btn-primary, .btn-primary:hover, .btn-warning, .btn-warning:hover, .btn-danger, .btn-danger:hover, .btn-success, .btn-success:hover, .btn-info, .btn-info:hover, .btn-inverse, .btn-inverse:hover { color: #ffffff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); } .btn-primary.active, .btn-warning.active, .btn-danger.active, .btn-success.active, .btn-info.active, .btn-inverse.active { color: rgba(255, 255, 255, 0.75); } .btn { border-color: #ccc; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); } .btn-primary { background-color: #0074cc; *background-color: #0055cc; background-image: -ms-linear-gradient(top, #0088cc, #0055cc); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc)); background-image: -webkit-linear-gradient(top, #0088cc, #0055cc); background-image: -o-linear-gradient(top, #0088cc, #0055cc); background-image: -moz-linear-gradient(top, #0088cc, #0055cc); background-image: linear-gradient(top, #0088cc, #0055cc); background-repeat: repeat-x; border-color: #0055cc #0055cc #003580; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { background-color: #0055cc; *background-color: #004ab3; } .btn-primary:active, .btn-primary.active { background-color: #004099 \9; } .btn-warning { background-color: #faa732; *background-color: #f89406; background-image: -ms-linear-gradient(top, #fbb450, #f89406); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); background-image: -webkit-linear-gradient(top, #fbb450, #f89406); background-image: -o-linear-gradient(top, #fbb450, #f89406); background-image: -moz-linear-gradient(top, #fbb450, #f89406); background-image: linear-gradient(top, #fbb450, #f89406); background-repeat: repeat-x; border-color: #f89406 #f89406 #ad6704; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-warning:hover, .btn-warning:active, .btn-warning.active, .btn-warning.disabled, .btn-warning[disabled] { background-color: #f89406; *background-color: #df8505; } .btn-warning:active, .btn-warning.active { background-color: #c67605 \9; } .btn-danger { background-color: #da4f49; *background-color: #bd362f; background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); background-image: linear-gradient(top, #ee5f5b, #bd362f); background-repeat: repeat-x; border-color: #bd362f #bd362f #802420; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-danger:hover, .btn-danger:active, .btn-danger.active, .btn-danger.disabled, .btn-danger[disabled] { background-color: #bd362f; *background-color: #a9302a; } .btn-danger:active, .btn-danger.active { background-color: #942a25 \9; } .btn-success { background-color: #5bb75b; *background-color: #51a351; background-image: -ms-linear-gradient(top, #62c462, #51a351); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); background-image: -webkit-linear-gradient(top, #62c462, #51a351); background-image: -o-linear-gradient(top, #62c462, #51a351); background-image: -moz-linear-gradient(top, #62c462, #51a351); background-image: linear-gradient(top, #62c462, #51a351); background-repeat: repeat-x; border-color: #51a351 #51a351 #387038; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-success:hover, .btn-success:active, .btn-success.active, .btn-success.disabled, .btn-success[disabled] { background-color: #51a351; *background-color: #499249; } .btn-success:active, .btn-success.active { background-color: #408140 \9; } .btn-info { background-color: #49afcd; *background-color: #2f96b4; background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); background-image: linear-gradient(top, #5bc0de, #2f96b4); background-repeat: repeat-x; border-color: #2f96b4 #2f96b4 #1f6377; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-info:hover, .btn-info:active, .btn-info.active, .btn-info.disabled, .btn-info[disabled] { background-color: #2f96b4; *background-color: #2a85a0; } .btn-info:active, .btn-info.active { background-color: #24748c \9; } .btn-inverse { background-color: #414141; *background-color: #222222; background-image: -ms-linear-gradient(top, #555555, #222222); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222)); background-image: -webkit-linear-gradient(top, #555555, #222222); background-image: -o-linear-gradient(top, #555555, #222222); background-image: -moz-linear-gradient(top, #555555, #222222); background-image: linear-gradient(top, #555555, #222222); background-repeat: repeat-x; border-color: #222222 #222222 #000000; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-inverse:hover, .btn-inverse:active, .btn-inverse.active, .btn-inverse.disabled, .btn-inverse[disabled] { background-color: #222222; *background-color: #151515; } .btn-inverse:active, .btn-inverse.active { background-color: #080808 \9; } button.btn, input[type="submit"].btn { *padding-top: 2px; *padding-bottom: 2px; } button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { padding: 0; border: 0; } button.btn.btn-large, input[type="submit"].btn.btn-large { *padding-top: 7px; *padding-bottom: 7px; } button.btn.btn-small, input[type="submit"].btn.btn-small { *padding-top: 3px; *padding-bottom: 3px; } button.btn.btn-mini, input[type="submit"].btn.btn-mini { *padding-top: 1px; *padding-bottom: 1px; } .btn-group { position: relative; *margin-left: .3em; *zoom: 1; } .btn-group:before, .btn-group:after { display: table; content: ""; } .btn-group:after { clear: both; } .btn-group:first-child { *margin-left: 0; } .btn-group + .btn-group { margin-left: 5px; } .btn-toolbar { margin-top: 9px; margin-bottom: 9px; } .btn-toolbar .btn-group { display: inline-block; *display: inline; /* IE7 inline-block hack */ *zoom: 1; } .btn-group > .btn { position: relative; float: left; margin-left: -1px; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .btn-group > .btn:first-child { margin-left: 0; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -moz-border-radius-topleft: 4px; } .btn-group > .btn:last-child, .btn-group > .dropdown-toggle { -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; -moz-border-radius-topright: 4px; -moz-border-radius-bottomright: 4px; } .btn-group > .btn.large:first-child { margin-left: 0; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px; -moz-border-radius-bottomleft: 6px; -moz-border-radius-topleft: 6px; } .btn-group > .btn.large:last-child, .btn-group > .large.dropdown-toggle { -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px; -moz-border-radius-topright: 6px; -moz-border-radius-bottomright: 6px; } .btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active { z-index: 2; } .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } .btn-group > .dropdown-toggle { *padding-top: 4px; padding-right: 8px; *padding-bottom: 4px; padding-left: 8px; -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn-group > .btn-mini.dropdown-toggle { padding-right: 5px; padding-left: 5px; } .btn-group > .btn-small.dropdown-toggle { *padding-top: 4px; *padding-bottom: 4px; } .btn-group > .btn-large.dropdown-toggle { padding-right: 12px; padding-left: 12px; } .btn-group.open .dropdown-toggle { background-image: none; -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn-group.open .btn.dropdown-toggle { background-color: #e6e6e6; } .btn-group.open .btn-primary.dropdown-toggle { background-color: #0055cc; } .btn-group.open .btn-warning.dropdown-toggle { background-color: #f89406; } .btn-group.open .btn-danger.dropdown-toggle { background-color: #bd362f; } .btn-group.open .btn-success.dropdown-toggle { background-color: #51a351; } .btn-group.open .btn-info.dropdown-toggle { background-color: #2f96b4; } .btn-group.open .btn-inverse.dropdown-toggle { background-color: #222222; } .btn .caret { margin-top: 7px; margin-left: 0; } .btn:hover .caret, .open.btn-group .caret { opacity: 1; filter: alpha(opacity=100); } .btn-mini .caret { margin-top: 5px; } .btn-small .caret { margin-top: 6px; } .btn-large .caret { margin-top: 6px; border-top-width: 5px; border-right-width: 5px; border-left-width: 5px; } .dropup .btn-large .caret { border-top: 0; border-bottom: 5px solid #000000; } .btn-primary .caret, .btn-warning .caret, .btn-danger .caret, .btn-info .caret, .btn-success .caret, .btn-inverse .caret { border-top-color: #ffffff; border-bottom-color: #ffffff; opacity: 0.75; filter: alpha(opacity=75); } .alert { padding: 8px 35px 8px 14px; margin-bottom: 18px; color: #c09853; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); background-color: #fcf8e3; border: 1px solid #fbeed5; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .alert-heading { color: inherit; } .alert .close { position: relative; top: -2px; right: -21px; line-height: 18px; } .alert-success { color: #468847; background-color: #dff0d8; border-color: #d6e9c6; } .alert-danger, .alert-error { color: #b94a48; background-color: #f2dede; border-color: #eed3d7; } .alert-info { color: #3a87ad; background-color: #d9edf7; border-color: #bce8f1; } .alert-block { padding-top: 14px; padding-bottom: 14px; } .alert-block > p, .alert-block > ul { margin-bottom: 0; } .alert-block p + p { margin-top: 5px; } .nav { margin-bottom: 18px; margin-left: 0; list-style: none; } .nav > li > a { display: block; } .nav > li > a:hover { text-decoration: none; background-color: #eeeeee; } .nav > .pull-right { float: right; } .nav .nav-header { display: block; padding: 3px 15px; font-size: 11px; font-weight: bold; line-height: 18px; color: #999999; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-transform: uppercase; } .nav li + .nav-header { margin-top: 9px; } .nav-list { padding-right: 15px; padding-left: 15px; margin-bottom: 0; } .nav-list > li > a, .nav-list .nav-header { margin-right: -15px; margin-left: -15px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); } .nav-list > li > a { padding: 3px 15px; } .nav-list > .active > a, .nav-list > .active > a:hover { color: #ffffff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); background-color: #0088cc; } .nav-list [class^="icon-"] { margin-right: 2px; } .nav-list .divider { *width: 100%; height: 1px; margin: 8px 1px; *margin: -5px 0 5px; overflow: hidden; background-color: #e5e5e5; border-bottom: 1px solid #ffffff; } .nav-tabs, .nav-pills { *zoom: 1; } .nav-tabs:before, .nav-pills:before, .nav-tabs:after, .nav-pills:after { display: table; content: ""; } .nav-tabs:after, .nav-pills:after { clear: both; } .nav-tabs > li, .nav-pills > li { float: left; } .nav-tabs > li > a, .nav-pills > li > a { padding-right: 12px; padding-left: 12px; margin-right: 2px; line-height: 14px; } .nav-tabs { border-bottom: 1px solid #ddd; } .nav-tabs > li { margin-bottom: -1px; } .nav-tabs > li > a { padding-top: 8px; padding-bottom: 8px; line-height: 18px; border: 1px solid transparent; -webkit-border-radius: 4px 4px 0 0; -moz-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { border-color: #eeeeee #eeeeee #dddddd; } .nav-tabs > .active > a, .nav-tabs > .active > a:hover { color: #555555; cursor: default; background-color: #ffffff; border: 1px solid #ddd; border-bottom-color: transparent; } .nav-pills > li > a { padding-top: 8px; padding-bottom: 8px; margin-top: 2px; margin-bottom: 2px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .nav-pills > .active > a, .nav-pills > .active > a:hover { color: #ffffff; background-color: #0088cc; } .nav-stacked > li { float: none; } .nav-stacked > li > a { margin-right: 0; } .nav-tabs.nav-stacked { border-bottom: 0; } .nav-tabs.nav-stacked > li > a { border: 1px solid #ddd; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .nav-tabs.nav-stacked > li:first-child > a { -webkit-border-radius: 4px 4px 0 0; -moz-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; } .nav-tabs.nav-stacked > li:last-child > a { -webkit-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px; } .nav-tabs.nav-stacked > li > a:hover { z-index: 2; border-color: #ddd; } .nav-pills.nav-stacked > li > a { margin-bottom: 3px; } .nav-pills.nav-stacked > li:last-child > a { margin-bottom: 1px; } .nav-tabs .dropdown-menu { -webkit-border-radius: 0 0 5px 5px; -moz-border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px; } .nav-pills .dropdown-menu { -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .nav-tabs .dropdown-toggle .caret, .nav-pills .dropdown-toggle .caret { margin-top: 6px; border-top-color: #0088cc; border-bottom-color: #0088cc; } .nav-tabs .dropdown-toggle:hover .caret, .nav-pills .dropdown-toggle:hover .caret { border-top-color: #005580; border-bottom-color: #005580; } .nav-tabs .active .dropdown-toggle .caret, .nav-pills .active .dropdown-toggle .caret { border-top-color: #333333; border-bottom-color: #333333; } .nav > .dropdown.active > a:hover { color: #000000; cursor: pointer; } .nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, .nav > li.dropdown.open.active > a:hover { color: #ffffff; background-color: #999999; border-color: #999999; } .nav li.dropdown.open .caret, .nav li.dropdown.open.active .caret, .nav li.dropdown.open a:hover .caret { border-top-color: #ffffff; border-bottom-color: #ffffff; opacity: 1; filter: alpha(opacity=100); } .tabs-stacked .open > a:hover { border-color: #999999; } .tabbable { *zoom: 1; } .tabbable:before, .tabbable:after { display: table; content: ""; } .tabbable:after { clear: both; } .tab-content { overflow: auto; } .tabs-below > .nav-tabs, .tabs-right > .nav-tabs, .tabs-left > .nav-tabs { border-bottom: 0; } .tab-content > .tab-pane, .pill-content > .pill-pane { display: none; } .tab-content > .active, .pill-content > .active { display: block; } .tabs-below > .nav-tabs { border-top: 1px solid #ddd; } .tabs-below > .nav-tabs > li { margin-top: -1px; margin-bottom: 0; } .tabs-below > .nav-tabs > li > a { -webkit-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px; } .tabs-below > .nav-tabs > li > a:hover { border-top-color: #ddd; border-bottom-color: transparent; } .tabs-below > .nav-tabs > .active > a, .tabs-below > .nav-tabs > .active > a:hover { border-color: transparent #ddd #ddd #ddd; } .tabs-left > .nav-tabs > li, .tabs-right > .nav-tabs > li { float: none; } .tabs-left > .nav-tabs > li > a, .tabs-right > .nav-tabs > li > a { min-width: 74px; margin-right: 0; margin-bottom: 3px; } .tabs-left > .nav-tabs { float: left; margin-right: 19px; border-right: 1px solid #ddd; } .tabs-left > .nav-tabs > li > a { margin-right: -1px; -webkit-border-radius: 4px 0 0 4px; -moz-border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px; } .tabs-left > .nav-tabs > li > a:hover { border-color: #eeeeee #dddddd #eeeeee #eeeeee; } .tabs-left > .nav-tabs .active > a, .tabs-left > .nav-tabs .active > a:hover { border-color: #ddd transparent #ddd #ddd; *border-right-color: #ffffff; } .tabs-right > .nav-tabs { float: right; margin-left: 19px; border-left: 1px solid #ddd; } .tabs-right > .nav-tabs > li > a { margin-left: -1px; -webkit-border-radius: 0 4px 4px 0; -moz-border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0; } .tabs-right > .nav-tabs > li > a:hover { border-color: #eeeeee #eeeeee #eeeeee #dddddd; } .tabs-right > .nav-tabs .active > a, .tabs-right > .nav-tabs .active > a:hover { border-color: #ddd #ddd #ddd transparent; *border-left-color: #ffffff; } .navbar { *position: relative; *z-index: 2; margin-bottom: 18px; overflow: visible; } .navbar-inner { min-height: 40px; padding-right: 20px; padding-left: 20px; background-color: #2c2c2c; background-image: -moz-linear-gradient(top, #333333, #222222); background-image: -ms-linear-gradient(top, #333333, #222222); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); background-image: -webkit-linear-gradient(top, #333333, #222222); background-image: -o-linear-gradient(top, #333333, #222222); background-image: linear-gradient(top, #333333, #222222); background-repeat: repeat-x; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); } .navbar .container { width: auto; } .nav-collapse.collapse { height: auto; } .navbar { color: #999999; } .navbar .brand:hover { text-decoration: none; } .navbar .brand { display: block; float: left; padding: 8px 20px 12px; margin-left: -20px; font-size: 20px; font-weight: 200; line-height: 1; color: #999999; } .navbar .navbar-text { margin-bottom: 0; line-height: 40px; } .navbar .navbar-link { color: #999999; } .navbar .navbar-link:hover { color: #ffffff; } .navbar .btn, .navbar .btn-group { margin-top: 5px; } .navbar .btn-group .btn { margin: 0; } .navbar-form { margin-bottom: 0; *zoom: 1; } .navbar-form:before, .navbar-form:after { display: table; content: ""; } .navbar-form:after { clear: both; } .navbar-form input, .navbar-form select, .navbar-form .radio, .navbar-form .checkbox { margin-top: 5px; } .navbar-form input, .navbar-form select { display: inline-block; margin-bottom: 0; } .navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { margin-top: 3px; } .navbar-form .input-append, .navbar-form .input-prepend { margin-top: 6px; white-space: nowrap; } .navbar-form .input-append input, .navbar-form .input-prepend input { margin-top: 0; } .navbar-search { position: relative; float: left; margin-top: 6px; margin-bottom: 0; } .navbar-search .search-query { padding: 4px 9px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 1; color: #ffffff; background-color: #626262; border: 1px solid #151515; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); -webkit-transition: none; -moz-transition: none; -ms-transition: none; -o-transition: none; transition: none; } .navbar-search .search-query:-moz-placeholder { color: #cccccc; } .navbar-search .search-query:-ms-input-placeholder { color: #cccccc; } .navbar-search .search-query::-webkit-input-placeholder { color: #cccccc; } .navbar-search .search-query:focus, .navbar-search .search-query.focused { padding: 5px 10px; color: #333333; text-shadow: 0 1px 0 #ffffff; background-color: #ffffff; border: 0; outline: 0; -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); } .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; right: 0; left: 0; z-index: 1030; margin-bottom: 0; } .navbar-fixed-top .navbar-inner, .navbar-fixed-bottom .navbar-inner { padding-right: 0; padding-left: 0; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .navbar-fixed-top .container, .navbar-fixed-bottom .container { width: 940px; } .navbar-fixed-top { top: 0; } .navbar-fixed-bottom { bottom: 0; } .navbar .nav { position: relative; left: 0; display: block; float: left; margin: 0 10px 0 0; } .navbar .nav.pull-right { float: right; } .navbar .nav > li { display: block; float: left; } .navbar .nav > li > a { float: none; padding: 9px 10px 11px; line-height: 19px; color: #999999; text-decoration: none; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); } .navbar .btn { display: inline-block; padding: 4px 10px 4px; margin: 5px 5px 6px; line-height: 18px; } .navbar .btn-group { padding: 5px 5px 6px; margin: 0; } .navbar .nav > li > a:hover { color: #ffffff; text-decoration: none; background-color: transparent; } .navbar .nav .active > a, .navbar .nav .active > a:hover { color: #ffffff; text-decoration: none; background-color: #222222; } .navbar .divider-vertical { width: 1px; height: 40px; margin: 0 9px; overflow: hidden; background-color: #222222; border-right: 1px solid #333333; } .navbar .nav.pull-right { margin-right: 0; margin-left: 10px; } .navbar .btn-navbar { display: none; float: right; padding: 7px 10px; margin-right: 5px; margin-left: 5px; background-color: #2c2c2c; *background-color: #222222; background-image: -ms-linear-gradient(top, #333333, #222222); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); background-image: -webkit-linear-gradient(top, #333333, #222222); background-image: -o-linear-gradient(top, #333333, #222222); background-image: linear-gradient(top, #333333, #222222); background-image: -moz-linear-gradient(top, #333333, #222222); background-repeat: repeat-x; border-color: #222222 #222222 #000000; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(enabled=false); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); } .navbar .btn-navbar:hover, .navbar .btn-navbar:active, .navbar .btn-navbar.active, .navbar .btn-navbar.disabled, .navbar .btn-navbar[disabled] { background-color: #222222; *background-color: #151515; } .navbar .btn-navbar:active, .navbar .btn-navbar.active { background-color: #080808 \9; } .navbar .btn-navbar .icon-bar { display: block; width: 18px; height: 2px; background-color: #f5f5f5; -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); } .btn-navbar .icon-bar + .icon-bar { margin-top: 3px; } .navbar .dropdown-menu:before { position: absolute; top: -7px; left: 9px; display: inline-block; border-right: 7px solid transparent; border-bottom: 7px solid #ccc; border-left: 7px solid transparent; border-bottom-color: rgba(0, 0, 0, 0.2); content: ''; } .navbar .dropdown-menu:after { position: absolute; top: -6px; left: 10px; display: inline-block; border-right: 6px solid transparent; border-bottom: 6px solid #ffffff; border-left: 6px solid transparent; content: ''; } .navbar-fixed-bottom .dropdown-menu:before { top: auto; bottom: -7px; border-top: 7px solid #ccc; border-bottom: 0; border-top-color: rgba(0, 0, 0, 0.2); } .navbar-fixed-bottom .dropdown-menu:after { top: auto; bottom: -6px; border-top: 6px solid #ffffff; border-bottom: 0; } .navbar .nav li.dropdown .dropdown-toggle .caret, .navbar .nav li.dropdown.open .caret { border-top-color: #ffffff; border-bottom-color: #ffffff; } .navbar .nav li.dropdown.active .caret { opacity: 1; filter: alpha(opacity=100); } .navbar .nav li.dropdown.open > .dropdown-toggle, .navbar .nav li.dropdown.active > .dropdown-toggle, .navbar .nav li.dropdown.open.active > .dropdown-toggle { background-color: transparent; } .navbar .nav li.dropdown.active > .dropdown-toggle:hover { color: #ffffff; } .navbar .pull-right .dropdown-menu, .navbar .dropdown-menu.pull-right { right: 0; left: auto; } .navbar .pull-right .dropdown-menu:before, .navbar .dropdown-menu.pull-right:before { right: 12px; left: auto; } .navbar .pull-right .dropdown-menu:after, .navbar .dropdown-menu.pull-right:after { right: 13px; left: auto; } .breadcrumb { padding: 7px 14px; margin: 0 0 18px; list-style: none; background-color: #fbfbfb; background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); background-image: linear-gradient(top, #ffffff, #f5f5f5); background-repeat: repeat-x; border: 1px solid #ddd; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; } .breadcrumb li { display: inline-block; *display: inline; text-shadow: 0 1px 0 #ffffff; *zoom: 1; } .breadcrumb .divider { padding: 0 5px; color: #999999; } .breadcrumb .active a { color: #333333; } .pagination { height: 36px; margin: 18px 0; } .pagination ul { display: inline-block; *display: inline; margin-bottom: 0; margin-left: 0; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; *zoom: 1; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .pagination li { display: inline; } .pagination a { float: left; padding: 0 14px; line-height: 34px; text-decoration: none; border: 1px solid #ddd; border-left-width: 0; } .pagination a:hover, .pagination .active a { background-color: #f5f5f5; } .pagination .active a { color: #999999; cursor: default; } .pagination .disabled span, .pagination .disabled a, .pagination .disabled a:hover { color: #999999; cursor: default; background-color: transparent; } .pagination li:first-child a { border-left-width: 1px; -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .pagination li:last-child a { -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .pagination-centered { text-align: center; } .pagination-right { text-align: right; } .pager { margin-bottom: 18px; margin-left: 0; text-align: center; list-style: none; *zoom: 1; } .pager:before, .pager:after { display: table; content: ""; } .pager:after { clear: both; } .pager li { display: inline; } .pager a { display: inline-block; padding: 5px 14px; background-color: #fff; border: 1px solid #ddd; -webkit-border-radius: 15px; -moz-border-radius: 15px; border-radius: 15px; } .pager a:hover { text-decoration: none; background-color: #f5f5f5; } .pager .next a { float: right; } .pager .previous a { float: left; } .pager .disabled a, .pager .disabled a:hover { color: #999999; cursor: default; background-color: #fff; } .modal-open .dropdown-menu { z-index: 2050; } .modal-open .dropdown.open { *z-index: 2050; } .modal-open .popover { z-index: 2060; } .modal-open .tooltip { z-index: 2070; } .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000000; } .modal-backdrop.fade { opacity: 0; } .modal-backdrop, .modal-backdrop.fade.in { opacity: 0.8; filter: alpha(opacity=80); } .modal { position: fixed; top: 50%; left: 50%; z-index: 1050; width: 560px; margin: -250px 0 0 -280px; overflow: auto; background-color: #ffffff; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, 0.3); *border: 1px solid #999; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -webkit-background-clip: padding-box; -moz-background-clip: padding-box; background-clip: padding-box; } .modal.fade { top: -25%; -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; -moz-transition: opacity 0.3s linear, top 0.3s ease-out; -ms-transition: opacity 0.3s linear, top 0.3s ease-out; -o-transition: opacity 0.3s linear, top 0.3s ease-out; transition: opacity 0.3s linear, top 0.3s ease-out; } .modal.fade.in { top: 50%; } .modal-header { padding: 9px 15px; border-bottom: 1px solid #eee; } .modal-header .close { margin-top: 2px; } .modal-body { max-height: 400px; padding: 15px; overflow-y: auto; } .modal-form { margin-bottom: 0; } .modal-footer { padding: 14px 15px 15px; margin-bottom: 0; text-align: right; background-color: #f5f5f5; border-top: 1px solid #ddd; -webkit-border-radius: 0 0 6px 6px; -moz-border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px; *zoom: 1; -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; } .modal-footer:before, .modal-footer:after { display: table; content: ""; } .modal-footer:after { clear: both; } .modal-footer .btn + .btn { margin-bottom: 0; margin-left: 5px; } .modal-footer .btn-group .btn + .btn { margin-left: -1px; } .tooltip { position: absolute; z-index: 1020; display: block; padding: 5px; font-size: 11px; opacity: 0; filter: alpha(opacity=0); visibility: visible; } .tooltip.in { opacity: 0.8; filter: alpha(opacity=80); } .tooltip.top { margin-top: -2px; } .tooltip.right { margin-left: 2px; } .tooltip.bottom { margin-top: 2px; } .tooltip.left { margin-left: -2px; } .tooltip.top .tooltip-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top: 5px solid #000000; border-right: 5px solid transparent; border-left: 5px solid transparent; } .tooltip.left .tooltip-arrow { top: 50%; right: 0; margin-top: -5px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 5px solid #000000; } .tooltip.bottom .tooltip-arrow { top: 0; left: 50%; margin-left: -5px; border-right: 5px solid transparent; border-bottom: 5px solid #000000; border-left: 5px solid transparent; } .tooltip.right .tooltip-arrow { top: 50%; left: 0; margin-top: -5px; border-top: 5px solid transparent; border-right: 5px solid #000000; border-bottom: 5px solid transparent; } .tooltip-inner { max-width: 200px; padding: 3px 8px; color: #ffffff; text-align: center; text-decoration: none; background-color: #000000; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .tooltip-arrow { position: absolute; width: 0; height: 0; } .popover { position: absolute; top: 0; left: 0; z-index: 1010; display: none; padding: 5px; } .popover.top { margin-top: -5px; } .popover.right { margin-left: 5px; } .popover.bottom { margin-top: 5px; } .popover.left { margin-left: -5px; } .popover.top .arrow { bottom: 0; left: 50%; margin-left: -5px; border-top: 5px solid #000000; border-right: 5px solid transparent; border-left: 5px solid transparent; } .popover.right .arrow { top: 50%; left: 0; margin-top: -5px; border-top: 5px solid transparent; border-right: 5px solid #000000; border-bottom: 5px solid transparent; } .popover.bottom .arrow { top: 0; left: 50%; margin-left: -5px; border-right: 5px solid transparent; border-bottom: 5px solid #000000; border-left: 5px solid transparent; } .popover.left .arrow { top: 50%; right: 0; margin-top: -5px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 5px solid #000000; } .popover .arrow { position: absolute; width: 0; height: 0; } .popover-inner { width: 280px; padding: 3px; overflow: hidden; background: #000000; background: rgba(0, 0, 0, 0.8); -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); } .popover-title { padding: 9px 15px; line-height: 1; background-color: #f5f5f5; border-bottom: 1px solid #eee; -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; } .popover-content { padding: 14px; background-color: #ffffff; -webkit-border-radius: 0 0 3px 3px; -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; -webkit-background-clip: padding-box; -moz-background-clip: padding-box; background-clip: padding-box; } .popover-content p, .popover-content ul, .popover-content ol { margin-bottom: 0; } .thumbnails { margin-left: -20px; list-style: none; *zoom: 1; } .thumbnails:before, .thumbnails:after { display: table; content: ""; } .thumbnails:after { clear: both; } .row-fluid .thumbnails { margin-left: 0; } .thumbnails > li { float: left; margin-bottom: 18px; margin-left: 20px; } .thumbnail { display: block; padding: 4px; line-height: 1; border: 1px solid #ddd; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); } a.thumbnail:hover { border-color: #0088cc; -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); } .thumbnail > img { display: block; max-width: 100%; margin-right: auto; margin-left: auto; } .thumbnail .caption { padding: 9px; } .label, .badge { font-size: 10.998px; font-weight: bold; line-height: 14px; color: #ffffff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); white-space: nowrap; vertical-align: baseline; background-color: #999999; } .label { padding: 1px 4px 2px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .badge { padding: 1px 9px 2px; -webkit-border-radius: 9px; -moz-border-radius: 9px; border-radius: 9px; } a.label:hover, a.badge:hover { color: #ffffff; text-decoration: none; cursor: pointer; } .label-important, .badge-important { background-color: #b94a48; } .label-important[href], .badge-important[href] { background-color: #953b39; } .label-warning, .badge-warning { background-color: #f89406; } .label-warning[href], .badge-warning[href] { background-color: #c67605; } .label-success, .badge-success { background-color: #468847; } .label-success[href], .badge-success[href] { background-color: #356635; } .label-info, .badge-info { background-color: #3a87ad; } .label-info[href], .badge-info[href] { background-color: #2d6987; } .label-inverse, .badge-inverse { background-color: #333333; } .label-inverse[href], .badge-inverse[href] { background-color: #1a1a1a; } @-webkit-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @-moz-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @-ms-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } @-o-keyframes progress-bar-stripes { from { background-position: 0 0; } to { background-position: 40px 0; } } @keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } .progress { height: 18px; margin-bottom: 18px; overflow: hidden; background-color: #f7f7f7; background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: linear-gradient(top, #f5f5f5, #f9f9f9); background-repeat: repeat-x; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } .progress .bar { width: 0; height: 18px; font-size: 12px; color: #ffffff; text-align: center; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #0e90d2; background-image: -moz-linear-gradient(top, #149bdf, #0480be); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); background-image: -webkit-linear-gradient(top, #149bdf, #0480be); background-image: -o-linear-gradient(top, #149bdf, #0480be); background-image: linear-gradient(top, #149bdf, #0480be); background-image: -ms-linear-gradient(top, #149bdf, #0480be); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; -webkit-transition: width 0.6s ease; -moz-transition: width 0.6s ease; -ms-transition: width 0.6s ease; -o-transition: width 0.6s ease; transition: width 0.6s ease; } .progress-striped .bar { background-color: #149bdf; background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -webkit-background-size: 40px 40px; -moz-background-size: 40px 40px; -o-background-size: 40px 40px; background-size: 40px 40px; } .progress.active .bar { -webkit-animation: progress-bar-stripes 2s linear infinite; -moz-animation: progress-bar-stripes 2s linear infinite; -ms-animation: progress-bar-stripes 2s linear infinite; -o-animation: progress-bar-stripes 2s linear infinite; animation: progress-bar-stripes 2s linear infinite; } .progress-danger .bar { background-color: #dd514c; background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); background-image: linear-gradient(top, #ee5f5b, #c43c35); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); } .progress-danger.progress-striped .bar { background-color: #ee5f5b; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-success .bar { background-color: #5eb95e; background-image: -moz-linear-gradient(top, #62c462, #57a957); background-image: -ms-linear-gradient(top, #62c462, #57a957); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); background-image: -webkit-linear-gradient(top, #62c462, #57a957); background-image: -o-linear-gradient(top, #62c462, #57a957); background-image: linear-gradient(top, #62c462, #57a957); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); } .progress-success.progress-striped .bar { background-color: #62c462; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-info .bar { background-color: #4bb1cf; background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); background-image: -o-linear-gradient(top, #5bc0de, #339bb9); background-image: linear-gradient(top, #5bc0de, #339bb9); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); } .progress-info.progress-striped .bar { background-color: #5bc0de; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-warning .bar { background-color: #faa732; background-image: -moz-linear-gradient(top, #fbb450, #f89406); background-image: -ms-linear-gradient(top, #fbb450, #f89406); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); background-image: -webkit-linear-gradient(top, #fbb450, #f89406); background-image: -o-linear-gradient(top, #fbb450, #f89406); background-image: linear-gradient(top, #fbb450, #f89406); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); } .progress-warning.progress-striped .bar { background-color: #fbb450; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .accordion { margin-bottom: 18px; } .accordion-group { margin-bottom: 2px; border: 1px solid #e5e5e5; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .accordion-heading { border-bottom: 0; } .accordion-heading .accordion-toggle { display: block; padding: 8px 15px; } .accordion-toggle { cursor: pointer; } .accordion-inner { padding: 9px 15px; border-top: 1px solid #e5e5e5; } .carousel { position: relative; margin-bottom: 18px; line-height: 1; } .carousel-inner { position: relative; width: 100%; overflow: hidden; } .carousel .item { position: relative; display: none; -webkit-transition: 0.6s ease-in-out left; -moz-transition: 0.6s ease-in-out left; -ms-transition: 0.6s ease-in-out left; -o-transition: 0.6s ease-in-out left; transition: 0.6s ease-in-out left; } .carousel .item > img { display: block; line-height: 1; } .carousel .active, .carousel .next, .carousel .prev { display: block; } .carousel .active { left: 0; } .carousel .next, .carousel .prev { position: absolute; top: 0; width: 100%; } .carousel .next { left: 100%; } .carousel .prev { left: -100%; } .carousel .next.left, .carousel .prev.right { left: 0; } .carousel .active.left { left: -100%; } .carousel .active.right { left: 100%; } .carousel-control { position: absolute; top: 40%; left: 15px; width: 40px; height: 40px; margin-top: -20px; font-size: 60px; font-weight: 100; line-height: 30px; color: #ffffff; text-align: center; background: #222222; border: 3px solid #ffffff; -webkit-border-radius: 23px; -moz-border-radius: 23px; border-radius: 23px; opacity: 0.5; filter: alpha(opacity=50); } .carousel-control.right { right: 15px; left: auto; } .carousel-control:hover { color: #ffffff; text-decoration: none; opacity: 0.9; filter: alpha(opacity=90); } .carousel-caption { position: absolute; right: 0; bottom: 0; left: 0; padding: 10px 15px 5px; background: #333333; background: rgba(0, 0, 0, 0.75); } .carousel-caption h4, .carousel-caption p { color: #ffffff; } .hero-unit { padding: 60px; margin-bottom: 30px; background-color: #eeeeee; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; } .hero-unit h1 { margin-bottom: 0; font-size: 60px; line-height: 1; letter-spacing: -1px; color: inherit; } .hero-unit p { font-size: 18px; font-weight: 200; line-height: 27px; color: inherit; } .pull-right { float: right; } .pull-left { float: left; } .hide { display: none; } .show { display: block; } .invisible { visibility: hidden; } ================================================ FILE: demo-websocket-socketio/src/main/resources/static/index.html ================================================ spring-boot-demo-websocket-socketio

    spring-boot-demo-websocket-socketio


    ================================================ FILE: demo-websocket-socketio/src/main/resources/static/js/socket.io/socket.io.js ================================================ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.io=e()}}(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&!this.encoding){var pack=this.packetBuffer.shift();this.packet(pack)}};Manager.prototype.cleanup=function(){var sub;while(sub=this.subs.shift())sub.destroy();this.packetBuffer=[];this.encoding=false;this.decoder.destroy()};Manager.prototype.close=Manager.prototype.disconnect=function(){this.skipReconnect=true;this.backoff.reset();this.readyState="closed";this.engine&&this.engine.close()};Manager.prototype.onclose=function(reason){debug("close");this.cleanup();this.backoff.reset();this.readyState="closed";this.emit("close",reason);if(this._reconnection&&!this.skipReconnect){this.reconnect()}};Manager.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var self=this;if(this.backoff.attempts>=this._reconnectionAttempts){debug("reconnect failed");this.backoff.reset();this.emitAll("reconnect_failed");this.reconnecting=false}else{var delay=this.backoff.duration();debug("will wait %dms before reconnect attempt",delay);this.reconnecting=true;var timer=setTimeout(function(){if(self.skipReconnect)return;debug("attempting reconnect");self.emitAll("reconnect_attempt",self.backoff.attempts);self.emitAll("reconnecting",self.backoff.attempts);if(self.skipReconnect)return;self.open(function(err){if(err){debug("reconnect attempt error");self.reconnecting=false;self.reconnect();self.emitAll("reconnect_error",err.data)}else{debug("reconnect success");self.onreconnect()}})},delay);this.subs.push({destroy:function(){clearTimeout(timer)}})}};Manager.prototype.onreconnect=function(){var attempt=this.backoff.attempts;this.reconnecting=false;this.backoff.reset();this.updateSocketIds();this.emitAll("reconnect",attempt)}},{"./on":4,"./socket":5,"./url":6,backo2:7,"component-bind":8,"component-emitter":9,debug:10,"engine.io-client":11,indexof:42,"object-component":43,"socket.io-parser":46}],4:[function(_dereq_,module,exports){module.exports=on;function on(obj,ev,fn){obj.on(ev,fn);return{destroy:function(){obj.removeListener(ev,fn)}}}},{}],5:[function(_dereq_,module,exports){var parser=_dereq_("socket.io-parser");var Emitter=_dereq_("component-emitter");var toArray=_dereq_("to-array");var on=_dereq_("./on");var bind=_dereq_("component-bind");var debug=_dereq_("debug")("socket.io-client:socket");var hasBin=_dereq_("has-binary");module.exports=exports=Socket;var events={connect:1,connect_error:1,connect_timeout:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1};var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};if(this.io.autoConnect)this.open();this.receiveBuffer=[];this.sendBuffer=[];this.connected=false;this.disconnected=true}Emitter(Socket.prototype);Socket.prototype.subEvents=function(){if(this.subs)return;var io=this.io;this.subs=[on(io,"open",bind(this,"onopen")),on(io,"packet",bind(this,"onpacket")),on(io,"close",bind(this,"onclose"))]};Socket.prototype.open=Socket.prototype.connect=function(){if(this.connected)return this;this.subEvents();this.io.open();if("open"==this.io.readyState)this.onopen();return this};Socket.prototype.send=function(){var args=toArray(arguments);args.unshift("message");this.emit.apply(this,args);return this};Socket.prototype.emit=function(ev){if(events.hasOwnProperty(ev)){emit.apply(this,arguments);return this}var args=toArray(arguments);var parserType=parser.EVENT;if(hasBin(args)){parserType=parser.BINARY_EVENT}var packet={type:parserType,data:args};if("function"==typeof args[args.length-1]){debug("emitting packet with ack id %d",this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++}if(this.connected){this.packet(packet)}else{this.sendBuffer.push(packet)}return this};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.packet(packet)};Socket.prototype.onopen=function(){debug("transport is open - connecting");if("/"!=this.nsp){this.packet({type:parser.CONNECT})}};Socket.prototype.onclose=function(reason){debug("close (%s)",reason);this.connected=false;this.disconnected=true;delete this.id;this.emit("disconnect",reason)};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.BINARY_EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.BINARY_ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit("error",packet.data);break}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug("emitting event %j",args);if(null!=packet.id){debug("attaching ack callback to event");args.push(this.ack(packet.id))}if(this.connected){emit.apply(this,args)}else{this.receiveBuffer.push(args)}};Socket.prototype.ack=function(id){var self=this;var sent=false;return function(){if(sent)return;sent=true;var args=toArray(arguments);debug("sending ack %j",args);var type=hasBin(args)?parser.BINARY_ACK:parser.ACK;self.packet({type:type,id:id,data:args})}};Socket.prototype.onack=function(packet){debug("calling ack %s with %j",packet.id,packet.data);var fn=this.acks[packet.id];fn.apply(this,packet.data);delete this.acks[packet.id]};Socket.prototype.onconnect=function(){this.connected=true;this.disconnected=false;this.emit("connect");this.emitBuffered()};Socket.prototype.emitBuffered=function(){var i;for(i=0;i0&&opts.jitter<=1?opts.jitter:0;this.attempts=0}Backoff.prototype.duration=function(){var ms=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var rand=Math.random();var deviation=Math.floor(rand*this.jitter*ms);ms=(Math.floor(rand*10)&1)==0?ms-deviation:ms+deviation}return Math.min(ms,this.max)|0};Backoff.prototype.reset=function(){this.attempts=0};Backoff.prototype.setMin=function(min){this.ms=min};Backoff.prototype.setMax=function(max){this.max=max};Backoff.prototype.setJitter=function(jitter){this.jitter=jitter}},{}],8:[function(_dereq_,module,exports){var slice=[].slice;module.exports=function(obj,fn){if("string"==typeof fn)fn=obj[fn];if("function"!=typeof fn)throw new Error("bind() requires a function");var args=slice.call(arguments,2);return function(){return fn.apply(obj,args.concat(slice.call(arguments)))}}},{}],9:[function(_dereq_,module,exports){module.exports=Emitter;function Emitter(obj){if(obj)return mixin(obj)}function mixin(obj){for(var key in Emitter.prototype){obj[key]=Emitter.prototype[key]}return obj}Emitter.prototype.on=Emitter.prototype.addEventListener=function(event,fn){this._callbacks=this._callbacks||{};(this._callbacks[event]=this._callbacks[event]||[]).push(fn);return this};Emitter.prototype.once=function(event,fn){var self=this;this._callbacks=this._callbacks||{};function on(){self.off(event,on);fn.apply(this,arguments)}on.fn=fn;this.on(event,on);return this};Emitter.prototype.off=Emitter.prototype.removeListener=Emitter.prototype.removeAllListeners=Emitter.prototype.removeEventListener=function(event,fn){this._callbacks=this._callbacks||{};if(0==arguments.length){this._callbacks={};return this}var callbacks=this._callbacks[event];if(!callbacks)return this;if(1==arguments.length){delete this._callbacks[event];return this}var cb;for(var i=0;i=hour)return(ms/hour).toFixed(1)+"h";if(ms>=min)return(ms/min).toFixed(1)+"m";if(ms>=sec)return(ms/sec|0)+"s";return ms+"ms"};debug.enabled=function(name){for(var i=0,len=debug.skips.length;i';iframe=document.createElement(html)}catch(e){iframe=document.createElement("iframe");iframe.name=self.iframeId;iframe.src="javascript:0"}iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe}initIframe();data=data.replace(rEscapedNewline,"\\\n");this.area.value=data.replace(rNewline,"\\n");try{this.form.submit()}catch(e){}if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=="complete"){complete()}}}else{this.iframe.onload=complete}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":18,"component-inherit":21}],17:[function(_dereq_,module,exports){(function(global){var XMLHttpRequest=_dereq_("xmlhttprequest");var Polling=_dereq_("./polling");var Emitter=_dereq_("component-emitter");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling-xhr");module.exports=XHR;module.exports.Request=Request;function empty(){}function XHR(opts){Polling.call(this,opts);if(global.location){var isSSL="https:"==location.protocol;var port=location.port;if(!port){port=isSSL?443:80}this.xd=opts.hostname!=global.location.hostname||port!=opts.port;this.xs=opts.secure!=isSSL}}inherit(XHR,Polling);XHR.prototype.supportsBinary=true;XHR.prototype.request=function(opts){opts=opts||{};opts.uri=this.uri();opts.xd=this.xd;opts.xs=this.xs;opts.agent=this.agent||false;opts.supportsBinary=this.supportsBinary;opts.enablesXDR=this.enablesXDR;opts.pfx=this.pfx;opts.key=this.key;opts.passphrase=this.passphrase;opts.cert=this.cert;opts.ca=this.ca;opts.ciphers=this.ciphers;opts.rejectUnauthorized=this.rejectUnauthorized;return new Request(opts)};XHR.prototype.doWrite=function(data,fn){var isBinary=typeof data!=="string"&&data!==undefined;var req=this.request({method:"POST",data:data,isBinary:isBinary});var self=this;req.on("success",fn);req.on("error",function(err){self.onError("xhr post error",err)});this.sendXhr=req};XHR.prototype.doPoll=function(){debug("xhr poll");var req=this.request();var self=this;req.on("data",function(data){self.onData(data)});req.on("error",function(err){self.onError("xhr poll error",err)});this.pollXhr=req};function Request(opts){this.method=opts.method||"GET";this.uri=opts.uri;this.xd=!!opts.xd;this.xs=!!opts.xs;this.async=false!==opts.async;this.data=undefined!=opts.data?opts.data:null;this.agent=opts.agent;this.isBinary=opts.isBinary;this.supportsBinary=opts.supportsBinary;this.enablesXDR=opts.enablesXDR;this.pfx=opts.pfx;this.key=opts.key;this.passphrase=opts.passphrase;this.cert=opts.cert;this.ca=opts.ca;this.ciphers=opts.ciphers;this.rejectUnauthorized=opts.rejectUnauthorized;this.create()}Emitter(Request.prototype);Request.prototype.create=function(){var opts={agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR};opts.pfx=this.pfx;opts.key=this.key;opts.passphrase=this.passphrase;opts.cert=this.cert;opts.ca=this.ca;opts.ciphers=this.ciphers;opts.rejectUnauthorized=this.rejectUnauthorized;var xhr=this.xhr=new XMLHttpRequest(opts);var self=this;try{debug("xhr open %s: %s",this.method,this.uri);xhr.open(this.method,this.uri,this.async);if(this.supportsBinary){xhr.responseType="arraybuffer"}if("POST"==this.method){try{if(this.isBinary){xhr.setRequestHeader("Content-type","application/octet-stream")}else{xhr.setRequestHeader("Content-type","text/plain;charset=UTF-8")}}catch(e){}}if("withCredentials"in xhr){xhr.withCredentials=true}if(this.hasXDR()){xhr.onload=function(){self.onLoad()};xhr.onerror=function(){self.onError(xhr.responseText)}}else{xhr.onreadystatechange=function(){if(4!=xhr.readyState)return;if(200==xhr.status||1223==xhr.status){self.onLoad()}else{setTimeout(function(){self.onError(xhr.status)},0)}}}debug("xhr data %s",this.data);xhr.send(this.data)}catch(e){setTimeout(function(){self.onError(e)},0);return}if(global.document){this.index=Request.requestsCount++;Request.requests[this.index]=this}};Request.prototype.onSuccess=function(){this.emit("success");this.cleanup()};Request.prototype.onData=function(data){this.emit("data",data);this.onSuccess()};Request.prototype.onError=function(err){this.emit("error",err);this.cleanup(true)};Request.prototype.cleanup=function(fromError){if("undefined"==typeof this.xhr||null===this.xhr){return}if(this.hasXDR()){this.xhr.onload=this.xhr.onerror=empty}else{this.xhr.onreadystatechange=empty}if(fromError){try{this.xhr.abort()}catch(e){}}if(global.document){delete Request.requests[this.index]}this.xhr=null};Request.prototype.onLoad=function(){var data;try{var contentType;try{contentType=this.xhr.getResponseHeader("Content-Type").split(";")[0]}catch(e){}if(contentType==="application/octet-stream"){data=this.xhr.response}else{if(!this.supportsBinary){data=this.xhr.responseText}else{data="ok"}}}catch(e){this.onError(e)}if(null!=data){this.onData(data)}};Request.prototype.hasXDR=function(){return"undefined"!==typeof global.XDomainRequest&&!this.xs&&this.enablesXDR};Request.prototype.abort=function(){this.cleanup()};if(global.document){Request.requestsCount=0;Request.requests={};if(global.attachEvent){global.attachEvent("onunload",unloadHandler)}else if(global.addEventListener){global.addEventListener("beforeunload",unloadHandler,false)}}function unloadHandler(){for(var i in Request.requests){if(Request.requests.hasOwnProperty(i)){Request.requests[i].abort()}}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":18,"component-emitter":9,"component-inherit":21,debug:22,xmlhttprequest:20}],18:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parseqs=_dereq_("parseqs");var parser=_dereq_("engine.io-parser");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling");module.exports=Polling;var hasXHR2=function(){var XMLHttpRequest=_dereq_("xmlhttprequest");var xhr=new XMLHttpRequest({xdomain:false});return null!=xhr.responseType}();function Polling(opts){var forceBase64=opts&&opts.forceBase64;if(!hasXHR2||forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(Polling,Transport);Polling.prototype.name="polling";Polling.prototype.doOpen=function(){this.poll()};Polling.prototype.pause=function(onPause){var pending=0;var self=this;this.readyState="pausing";function pause(){debug("paused");self.readyState="paused";onPause()}if(this.polling||!this.writable){var total=0;if(this.polling){debug("we are currently polling - waiting to pause");total++;this.once("pollComplete",function(){debug("pre-pause polling complete");--total||pause()})}if(!this.writable){debug("we are currently writing - waiting to pause");total++;this.once("drain",function(){debug("pre-pause writing complete");--total||pause()})}}else{pause()}};Polling.prototype.poll=function(){debug("polling");this.polling=true;this.doPoll();this.emit("poll")};Polling.prototype.onData=function(data){var self=this;debug("polling got data %s",data);var callback=function(packet,index,total){if("opening"==self.readyState){self.onOpen()}if("close"==packet.type){self.onClose();return false}self.onPacket(packet)};parser.decodePayload(data,this.socket.binaryType,callback);if("closed"!=this.readyState){this.polling=false;this.emit("pollComplete");if("open"==this.readyState){this.poll()}else{debug('ignoring poll - transport state "%s"',this.readyState)}}};Polling.prototype.doClose=function(){var self=this;function close(){debug("writing close packet");self.write([{type:"close"}])}if("open"==this.readyState){debug("transport open - closing");close()}else{debug("transport not open - deferring close");this.once("open",close)}};Polling.prototype.write=function(packets){var self=this;this.writable=false;var callbackfn=function(){self.writable=true;self.emit("drain")};var self=this;parser.encodePayload(packets,this.supportsBinary,function(data){self.doWrite(data,callbackfn)})};Polling.prototype.uri=function(){var query=this.query||{};var schema=this.secure?"https":"http";var port="";if(false!==this.timestampRequests){query[this.timestampParam]=+new Date+"-"+Transport.timestamps++}if(!this.supportsBinary&&!query.sid){query.b64=1}query=parseqs.encode(query);if(this.port&&("https"==schema&&this.port!=443||"http"==schema&&this.port!=80)){port=":"+this.port}if(query.length){query="?"+query}return schema+"://"+this.hostname+port+this.path+query}},{"../transport":14,"component-inherit":21,debug:22,"engine.io-parser":25,parseqs:35,xmlhttprequest:20}],19:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parser=_dereq_("engine.io-parser");var parseqs=_dereq_("parseqs");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:websocket");var WebSocket=_dereq_("ws");module.exports=WS;function WS(opts){var forceBase64=opts&&opts.forceBase64;if(forceBase64){this.supportsBinary=false}this.perMessageDeflate=opts.perMessageDeflate;Transport.call(this,opts)}inherit(WS,Transport);WS.prototype.name="websocket";WS.prototype.supportsBinary=true;WS.prototype.doOpen=function(){if(!this.check()){return}var self=this;var uri=this.uri();var protocols=void 0;var opts={agent:this.agent,perMessageDeflate:this.perMessageDeflate};opts.pfx=this.pfx;opts.key=this.key;opts.passphrase=this.passphrase;opts.cert=this.cert;opts.ca=this.ca;opts.ciphers=this.ciphers;opts.rejectUnauthorized=this.rejectUnauthorized;this.ws=new WebSocket(uri,protocols,opts);if(this.ws.binaryType===undefined){this.supportsBinary=false}this.ws.binaryType="arraybuffer";this.addEventListeners()};WS.prototype.addEventListeners=function(){var self=this;this.ws.onopen=function(){self.onOpen()};this.ws.onclose=function(){self.onClose()};this.ws.onmessage=function(ev){self.onData(ev.data)};this.ws.onerror=function(e){self.onError("websocket error",e)}};if("undefined"!=typeof navigator&&/iPad|iPhone|iPod/i.test(navigator.userAgent)){WS.prototype.onData=function(data){var self=this;setTimeout(function(){Transport.prototype.onData.call(self,data)},0)}}WS.prototype.write=function(packets){var self=this;this.writable=false;for(var i=0,l=packets.length;i=31}exports.formatters.j=function(v){return JSON.stringify(v)};function formatArgs(){var args=arguments;var useColors=this.useColors;args[0]=(useColors?"%c":"")+this.namespace+(useColors?" %c":" ")+args[0]+(useColors?"%c ":" ")+"+"+exports.humanize(this.diff);if(!useColors)return args;var c="color: "+this.color;args=[args[0],c,"color: inherit"].concat(Array.prototype.slice.call(args,1));var index=0;var lastC=0;args[0].replace(/%[a-z%]/g,function(match){if("%"===match)return;index++;if("%c"===match){lastC=index}});args.splice(lastC,0,c);return args}function log(){return"object"==typeof console&&"function"==typeof console.log&&Function.prototype.apply.call(console.log,console,arguments)}function save(namespaces){try{if(null==namespaces){localStorage.removeItem("debug")}else{localStorage.debug=namespaces}}catch(e){}}function load(){var r;try{r=localStorage.debug}catch(e){}return r}exports.enable(load())},{"./debug":23}],23:[function(_dereq_,module,exports){exports=module.exports=debug;exports.coerce=coerce;exports.disable=disable;exports.enable=enable;exports.enabled=enabled;exports.humanize=_dereq_("ms");exports.names=[];exports.skips=[];exports.formatters={};var prevColor=0;var prevTime;function selectColor(){return exports.colors[prevColor++%exports.colors.length]}function debug(namespace){function disabled(){}disabled.enabled=false;function enabled(){var self=enabled;var curr=+new Date;var ms=curr-(prevTime||curr);self.diff=ms;self.prev=prevTime;self.curr=curr;prevTime=curr;if(null==self.useColors)self.useColors=exports.useColors();if(null==self.color&&self.useColors)self.color=selectColor();var args=Array.prototype.slice.call(arguments);args[0]=exports.coerce(args[0]);if("string"!==typeof args[0]){args=["%o"].concat(args)}var index=0;args[0]=args[0].replace(/%([a-z%])/g,function(match,format){if(match==="%")return match;index++;var formatter=exports.formatters[format];if("function"===typeof formatter){var val=args[index];match=formatter.call(self,val);args.splice(index,1);index--}return match});if("function"===typeof exports.formatArgs){args=exports.formatArgs.apply(self,args)}var logFn=enabled.log||exports.log||console.log.bind(console);logFn.apply(self,args)}enabled.enabled=true;var fn=exports.enabled(namespace)?enabled:disabled;fn.namespace=namespace;return fn}function enable(namespaces){exports.save(namespaces);var split=(namespaces||"").split(/[\s,]+/);var len=split.length;for(var i=0;i=d)return Math.round(ms/d)+"d";if(ms>=h)return Math.round(ms/h)+"h";if(ms>=m)return Math.round(ms/m)+"m";if(ms>=s)return Math.round(ms/s)+"s";return ms+"ms"}function long(ms){return plural(ms,d,"day")||plural(ms,h,"hour")||plural(ms,m,"minute")||plural(ms,s,"second")||ms+" ms"}function plural(ms,n,name){if(ms1){return{type:packetslist[type],data:data.substring(1)}}else{return{type:packetslist[type]}}}var asArray=new Uint8Array(data);var type=asArray[0];var rest=sliceBuffer(data,1);if(Blob&&binaryType==="blob"){rest=new Blob([rest])}return{type:packetslist[type],data:rest}};exports.decodeBase64Packet=function(msg,binaryType){var type=packetslist[msg.charAt(0)];if(!global.ArrayBuffer){return{type:type,data:{base64:true,data:msg.substr(1)}}}var data=base64encoder.decode(msg.substr(1));if(binaryType==="blob"&&Blob){data=new Blob([data])}return{type:type,data:data}};exports.encodePayload=function(packets,supportsBinary,callback){if(typeof supportsBinary=="function"){callback=supportsBinary;supportsBinary=null}var isBinary=hasBinary(packets);if(supportsBinary&&isBinary){if(Blob&&!dontSendBlobs){return exports.encodePayloadAsBlob(packets,callback)}return exports.encodePayloadAsArrayBuffer(packets,callback)}if(!packets.length){return callback("0:")}function setLengthHeader(message){return message.length+":"+message}function encodeOne(packet,doneCallback){exports.encodePacket(packet,!isBinary?false:supportsBinary,true,function(message){doneCallback(null,setLengthHeader(message))})}map(packets,encodeOne,function(err,results){return callback(results.join(""))})};function map(ary,each,done){var result=new Array(ary.length);var next=after(ary.length,done);var eachWithIndex=function(i,el,cb){each(el,function(error,msg){result[i]=msg;cb(error,result)})};for(var i=0;i0){var tailArray=new Uint8Array(bufferTail);var isString=tailArray[0]===0;var msgLength="";for(var i=1;;i++){if(tailArray[i]==255)break;if(msgLength.length>310){numberTooLong=true;break}msgLength+=tailArray[i]}if(numberTooLong)return callback(err,0,1);bufferTail=sliceBuffer(bufferTail,2+msgLength.length);msgLength=parseInt(msgLength);var msg=sliceBuffer(bufferTail,0,msgLength);if(isString){try{msg=String.fromCharCode.apply(null,new Uint8Array(msg))}catch(e){var typed=new Uint8Array(msg);msg="";for(var i=0;ibytes){end=bytes}if(start>=bytes||start>=end||bytes===0){return new ArrayBuffer(0)}var abv=new Uint8Array(arraybuffer);var result=new Uint8Array(end-start);for(var i=start,ii=0;i>2];base64+=chars[(bytes[i]&3)<<4|bytes[i+1]>>4];base64+=chars[(bytes[i+1]&15)<<2|bytes[i+2]>>6];base64+=chars[bytes[i+2]&63]}if(len%3===2){base64=base64.substring(0,base64.length-1)+"="}else if(len%3===1){base64=base64.substring(0,base64.length-2)+"=="}return base64};exports.decode=function(base64){var bufferLength=base64.length*.75,len=base64.length,i,p=0,encoded1,encoded2,encoded3,encoded4;if(base64[base64.length-1]==="="){bufferLength--;if(base64[base64.length-2]==="="){bufferLength--}}var arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer);for(i=0;i>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer}})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},{}],30:[function(_dereq_,module,exports){(function(global){var BlobBuilder=global.BlobBuilder||global.WebKitBlobBuilder||global.MSBlobBuilder||global.MozBlobBuilder;var blobSupported=function(){try{var b=new Blob(["hi"]);return b.size==2}catch(e){return false}}();var blobBuilderSupported=BlobBuilder&&BlobBuilder.prototype.append&&BlobBuilder.prototype.getBlob;function BlobBuilderConstructor(ary,options){options=options||{};var bb=new BlobBuilder;for(var i=0;i=55296&&value<=56319&&counter65535){value-=65536;output+=stringFromCharCode(value>>>10&1023|55296);value=56320|value&1023}output+=stringFromCharCode(value)}return output}function createByte(codePoint,shift){return stringFromCharCode(codePoint>>shift&63|128)}function encodeCodePoint(codePoint){if((codePoint&4294967168)==0){return stringFromCharCode(codePoint)}var symbol="";if((codePoint&4294965248)==0){symbol=stringFromCharCode(codePoint>>6&31|192)}else if((codePoint&4294901760)==0){symbol=stringFromCharCode(codePoint>>12&15|224);symbol+=createByte(codePoint,6)}else if((codePoint&4292870144)==0){symbol=stringFromCharCode(codePoint>>18&7|240);symbol+=createByte(codePoint,12);symbol+=createByte(codePoint,6)}symbol+=stringFromCharCode(codePoint&63|128);return symbol}function utf8encode(string){var codePoints=ucs2decode(string);var length=codePoints.length;var index=-1;var codePoint;var byteString="";while(++index=byteCount){throw Error("Invalid byte index")}var continuationByte=byteArray[byteIndex]&255;byteIndex++;if((continuationByte&192)==128){return continuationByte&63}throw Error("Invalid continuation byte")}function decodeSymbol(){var byte1;var byte2;var byte3;var byte4;var codePoint;if(byteIndex>byteCount){throw Error("Invalid byte index")}if(byteIndex==byteCount){return false}byte1=byteArray[byteIndex]&255;byteIndex++;if((byte1&128)==0){return byte1}if((byte1&224)==192){var byte2=readContinuationByte();codePoint=(byte1&31)<<6|byte2;if(codePoint>=128){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&240)==224){byte2=readContinuationByte();byte3=readContinuationByte();codePoint=(byte1&15)<<12|byte2<<6|byte3;if(codePoint>=2048){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&248)==240){byte2=readContinuationByte();byte3=readContinuationByte();byte4=readContinuationByte();codePoint=(byte1&15)<<18|byte2<<12|byte3<<6|byte4;if(codePoint>=65536&&codePoint<=1114111){return codePoint}}throw Error("Invalid UTF-8 detected")}var byteArray;var byteCount;var byteIndex;function utf8decode(byteString){byteArray=ucs2decode(byteString);byteCount=byteArray.length;byteIndex=0;var codePoints=[];var tmp;while((tmp=decodeSymbol())!==false){codePoints.push(tmp)}return ucs2encode(codePoints)}var utf8={version:"2.0.0",encode:utf8encode,decode:utf8decode};if(typeof define=="function"&&typeof define.amd=="object"&&define.amd){define(function(){return utf8})}else if(freeExports&&!freeExports.nodeType){if(freeModule){freeModule.exports=utf8}else{var object={};var hasOwnProperty=object.hasOwnProperty;for(var key in utf8){hasOwnProperty.call(utf8,key)&&(freeExports[key]=utf8[key])}}}else{root.utf8=utf8}})(this)}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],34:[function(_dereq_,module,exports){(function(global){var rvalidchars=/^[\],:{}\s]*$/;var rvalidescape=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rvalidtokens=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rvalidbraces=/(?:^|:|,)(?:\s*\[)+/g;var rtrimLeft=/^\s+/;var rtrimRight=/\s+$/;module.exports=function parsejson(data){if("string"!=typeof data||!data){return null}data=data.replace(rtrimLeft,"").replace(rtrimRight,"");if(global.JSON&&JSON.parse){return JSON.parse(data)}if(rvalidchars.test(data.replace(rvalidescape,"@").replace(rvalidtokens,"]").replace(rvalidbraces,""))){return new Function("return "+data)()}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],35:[function(_dereq_,module,exports){exports.encode=function(obj){var str="";for(var i in obj){if(obj.hasOwnProperty(i)){if(str.length)str+="&";str+=encodeURIComponent(i)+"="+encodeURIComponent(obj[i])}}return str};exports.decode=function(qs){var qry={};var pairs=qs.split("&");for(var i=0,l=pairs.length;i1)))/4)-floor((year-1901+month)/100)+floor((year-1601+month)/400)}}if(!(isProperty={}.hasOwnProperty)){isProperty=function(property){var members={},constructor;if((members.__proto__=null,members.__proto__={toString:1},members).toString!=getClass){isProperty=function(property){var original=this.__proto__,result=property in(this.__proto__=null,this);this.__proto__=original;return result}}else{constructor=members.constructor;isProperty=function(property){var parent=(this.constructor||constructor).prototype;return property in this&&!(property in parent&&this[property]===parent[property])}}members=null;return isProperty.call(this,property)}}var PrimitiveTypes={"boolean":1,number:1,string:1,undefined:1};var isHostType=function(object,property){var type=typeof object[property];return type=="object"?!!object[property]:!PrimitiveTypes[type]};forEach=function(object,callback){var size=0,Properties,members,property;(Properties=function(){this.valueOf=0}).prototype.valueOf=0;members=new Properties;for(property in members){if(isProperty.call(members,property)){size++}}Properties=members=null;if(!size){members=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,length;var hasProperty=!isFunction&&typeof object.constructor!="function"&&isHostType(object,"hasOwnProperty")?object.hasOwnProperty:isProperty;for(property in object){if(!(isFunction&&property=="prototype")&&hasProperty.call(object,property)){callback(property)}}for(length=members.length;property=members[--length];hasProperty.call(object,property)&&callback(property));}}else if(size==2){forEach=function(object,callback){var members={},isFunction=getClass.call(object)==functionClass,property;for(property in object){if(!(isFunction&&property=="prototype")&&!isProperty.call(members,property)&&(members[property]=1)&&isProperty.call(object,property)){callback(property)}}}}else{forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,isConstructor;for(property in object){if(!(isFunction&&property=="prototype")&&isProperty.call(object,property)&&!(isConstructor=property==="constructor")){callback(property)}}if(isConstructor||isProperty.call(object,property="constructor")){callback(property)}}}return forEach(object,callback)};if(!has("json-stringify")){var Escapes={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};var leadingZeroes="000000";var toPaddedString=function(width,value){return(leadingZeroes+(value||0)).slice(-width)};var unicodePrefix="\\u00";var quote=function(value){var result='"',index=0,length=value.length,isLarge=length>10&&charIndexBuggy,symbols;if(isLarge){symbols=value.split("")}for(;index-1/0&&value<1/0){if(getDay){date=floor(value/864e5);for(year=floor(date/365.2425)+1970-1;getDay(year+1,0)<=date;year++);for(month=floor((date-getDay(year,0))/30.42);getDay(year,month+1)<=date;month++);date=1+date-getDay(year,month);time=(value%864e5+864e5)%864e5;hours=floor(time/36e5)%24;minutes=floor(time/6e4)%60;seconds=floor(time/1e3)%60;milliseconds=time%1e3}else{year=value.getUTCFullYear();month=value.getUTCMonth();date=value.getUTCDate();hours=value.getUTCHours();minutes=value.getUTCMinutes();seconds=value.getUTCSeconds();milliseconds=value.getUTCMilliseconds()}value=(year<=0||year>=1e4?(year<0?"-":"+")+toPaddedString(6,year<0?-year:year):toPaddedString(4,year))+"-"+toPaddedString(2,month+1)+"-"+toPaddedString(2,date)+"T"+toPaddedString(2,hours)+":"+toPaddedString(2,minutes)+":"+toPaddedString(2,seconds)+"."+toPaddedString(3,milliseconds)+"Z"}else{value=null}}else if(typeof value.toJSON=="function"&&(className!=numberClass&&className!=stringClass&&className!=arrayClass||isProperty.call(value,"toJSON"))){value=value.toJSON(property)}}if(callback){value=callback.call(object,property,value)}if(value===null){return"null"}className=getClass.call(value);if(className==booleanClass){return""+value}else if(className==numberClass){return value>-1/0&&value<1/0?""+value:"null"}else if(className==stringClass){return quote(""+value)}if(typeof value=="object"){for(length=stack.length;length--;){if(stack[length]===value){throw TypeError()}}stack.push(value);results=[];prefix=indentation;indentation+=whitespace;if(className==arrayClass){for(index=0,length=value.length;index0){for(whitespace="",width>10&&(width=10);whitespace.length=48&&charCode<=57||charCode>=97&&charCode<=102||charCode>=65&&charCode<=70)){abort()}}value+=fromCharCode("0x"+source.slice(begin,Index));break;default:abort()}}else{if(charCode==34){break}charCode=source.charCodeAt(Index);begin=Index;while(charCode>=32&&charCode!=92&&charCode!=34){charCode=source.charCodeAt(++Index)}value+=source.slice(begin,Index)}}if(source.charCodeAt(Index)==34){Index++;return value}abort();default:begin=Index;if(charCode==45){isSigned=true;charCode=source.charCodeAt(++Index)}if(charCode>=48&&charCode<=57){if(charCode==48&&(charCode=source.charCodeAt(Index+1),charCode>=48&&charCode<=57)){abort()}isSigned=false;for(;Index=48&&charCode<=57);Index++);if(source.charCodeAt(Index)==46){position=++Index;for(;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}charCode=source.charCodeAt(Index);if(charCode==101||charCode==69){charCode=source.charCodeAt(++Index);if(charCode==43||charCode==45){Index++}for(position=Index;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}return+source.slice(begin,Index)}if(isSigned){abort()}if(source.slice(Index,Index+4)=="true"){Index+=4;return true}else if(source.slice(Index,Index+5)=="false"){Index+=5;return false}else if(source.slice(Index,Index+4)=="null"){Index+=4;return null}abort()}}return"$"};var get=function(value){var results,hasMembers;if(value=="$"){abort()}if(typeof value=="string"){if((charIndexBuggy?value.charAt(0):value[0])=="@"){return value.slice(1)}if(value=="["){results=[];for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="]"){break}if(hasMembers){if(value==","){value=lex();if(value=="]"){abort()}}else{abort()}}if(value==","){abort()}results.push(get(value))}return results}else if(value=="{"){results={};for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="}"){break}if(hasMembers){if(value==","){value=lex();if(value=="}"){abort()}}else{abort()}}if(value==","||typeof value!="string"||(charIndexBuggy?value.charAt(0):value[0])!="@"||lex()!=":"){abort()}results[value.slice(1)]=get(lex())}return results}abort()}return value};var update=function(source,property,callback){var element=walk(source,property,callback);if(element===undef){delete source[property]}else{source[property]=element}};var walk=function(source,property,callback){var value=source[property],length;if(typeof value=="object"&&value){if(getClass.call(value)==arrayClass){for(length=value.length;length--;){update(value,length,callback)}}else{forEach(value,function(property){update(value,property,callback)})}}return callback.call(source,property,value)};JSON3.parse=function(source,callback){var result,value;Index=0;Source=""+source;result=get(lex());if(lex()!="$"){abort()}Index=Source=null;return callback&&getClass.call(callback)==functionClass?walk((value={},value[""]=result,value),"",callback):result}}}if(isLoader){define(function(){return JSON3})}})(this)},{}],50:[function(_dereq_,module,exports){module.exports=toArray;function toArray(list,index){var array=[];index=index||0;for(var i=index||0;i 此 demo 主要演示了如何使用 Spring Boot 集成 Zookeeper 结合AOP实现分布式锁。 ## pom.xml ```xml 4.0.0 spring-boot-demo-zookeeper 1.0.0-SNAPSHOT jar spring-boot-demo-zookeeper 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-configuration-processor true org.springframework.boot spring-boot-starter-aop org.apache.curator curator-recipes 4.1.0 cn.hutool hutool-all org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true spring-boot-demo-zookeeper org.springframework.boot spring-boot-maven-plugin ``` ## ZkProps.java ```java /** *

    * Zookeeper 配置项 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:47 */ @Data @ConfigurationProperties(prefix = "zk") public class ZkProps { /** * 连接地址 */ private String url; /** * 超时时间(毫秒),默认1000 */ private int timeout = 1000; /** * 重试次数,默认3 */ private int retry = 3; } ``` ## application.yml ```yaml server: port: 8080 servlet: context-path: /demo zk: url: 127.0.0.1:2181 timeout: 1000 retry: 3 ``` ## ZkConfig.java ```java /** *

    * Zookeeper配置类 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:45 */ @Configuration @EnableConfigurationProperties(ZkProps.class) public class ZkConfig { private final ZkProps zkProps; @Autowired public ZkConfig(ZkProps zkProps) { this.zkProps = zkProps; } @Bean public CuratorFramework curatorFramework() { RetryPolicy retryPolicy = new ExponentialBackoffRetry(zkProps.getTimeout(), zkProps.getRetry()); CuratorFramework client = CuratorFrameworkFactory.newClient(zkProps.getUrl(), retryPolicy); client.start(); return client; } } ``` ## ZooLock.java > 分布式锁的关键注解 ```java /** *

    * 基于Zookeeper的分布式锁注解 * 在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:11 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface ZooLock { /** * 分布式锁的键 */ String key(); /** * 锁释放时间,默认五秒 */ long timeout() default 5 * 1000; /** * 时间格式,默认:毫秒 */ TimeUnit timeUnit() default TimeUnit.MILLISECONDS; } ``` ## LockKeyParam.java > 分布式锁动态key的关键注解 ```java /** *

    * 分布式锁动态key注解,配置之后key的值会动态获取参数内容 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:17 */ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface LockKeyParam { /** * 如果动态key在user对象中,那么就需要设置fields的值为user对象中的属性名可以为多个,基本类型则不需要设置该值 *

    例1:public void count(@LockKeyParam({"id"}) User user) *

    例2:public void count(@LockKeyParam({"id","userName"}) User user) *

    例3:public void count(@LockKeyParam String userId) */ String[] fields() default {}; } ``` ## ZooLockAspect.java > 分布式锁的关键部分 ```java /** *

    * 使用 aop 切面记录请求日志信息 *

    * * @author yangkai.shen * @date Created in 2018-10-01 22:05 */ @Aspect @Component @Slf4j public class ZooLockAspect { private final CuratorFramework zkClient; private static final String KEY_PREFIX = "DISTRIBUTED_LOCK_"; private static final String KEY_SEPARATOR = "/"; @Autowired public ZooLockAspect(CuratorFramework zkClient) { this.zkClient = zkClient; } /** * 切入点 */ @Pointcut("@annotation(com.xkcoding.zookeeper.annotation.ZooLock)") public void doLock() { } /** * 环绕操作 * * @param point 切入点 * @return 原方法返回值 * @throws Throwable 异常信息 */ @Around("doLock()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Object[] args = point.getArgs(); ZooLock zooLock = method.getAnnotation(ZooLock.class); if (StrUtil.isBlank(zooLock.key())) { throw new RuntimeException("分布式锁键不能为空"); } String lockKey = buildLockKey(zooLock, method, args); InterProcessMutex lock = new InterProcessMutex(zkClient, lockKey); try { // 假设上锁成功,以后拿到的都是 false if (lock.acquire(zooLock.timeout(), zooLock.timeUnit())) { return point.proceed(); } else { throw new RuntimeException("请勿重复提交"); } } finally { lock.release(); } } /** * 构造分布式锁的键 * * @param lock 注解 * @param method 注解标记的方法 * @param args 方法上的参数 * @return * @throws NoSuchFieldException * @throws IllegalAccessException */ private String buildLockKey(ZooLock lock, Method method, Object[] args) throws NoSuchFieldException, IllegalAccessException { StringBuilder key = new StringBuilder(KEY_SEPARATOR + KEY_PREFIX + lock.key()); // 迭代全部参数的注解,根据使用LockKeyParam的注解的参数所在的下标,来获取args中对应下标的参数值拼接到前半部分key上 Annotation[][] parameterAnnotations = method.getParameterAnnotations(); for (int i = 0; i < parameterAnnotations.length; i++) { // 循环该参数全部注解 for (Annotation annotation : parameterAnnotations[i]) { // 注解不是 @LockKeyParam if (!annotation.annotationType().isInstance(LockKeyParam.class)) { continue; } // 获取所有fields String[] fields = ((LockKeyParam) annotation).fields(); if (ArrayUtil.isEmpty(fields)) { // 普通数据类型直接拼接 if (ObjectUtil.isNull(args[i])) { throw new RuntimeException("动态参数不能为null"); } key.append(KEY_SEPARATOR).append(args[i]); } else { // @LockKeyParam的fields值不为null,所以当前参数应该是对象类型 for (String field : fields) { Class clazz = args[i].getClass(); Field declaredField = clazz.getDeclaredField(field); declaredField.setAccessible(true); Object value = declaredField.get(clazz); key.append(KEY_SEPARATOR).append(value); } } } } return key.toString(); } } ``` ## SpringBootDemoZookeeperApplicationTests.java > 测试分布式锁 ```java @RunWith(SpringRunner.class) @SpringBootTest @Slf4j public class SpringBootDemoZookeeperApplicationTests { public Integer getCount() { return count; } private Integer count = 10000; private ExecutorService executorService = Executors.newFixedThreadPool(1000); @Autowired private CuratorFramework zkClient; /** * 不使用分布式锁,程序结束查看count的值是否为0 */ @Test public void test() throws InterruptedException { IntStream.range(0, 10000).forEach(i -> executorService.execute(this::doBuy)); TimeUnit.MINUTES.sleep(1); log.error("count值为{}", count); } /** * 测试AOP分布式锁 */ @Test public void testAopLock() throws InterruptedException { // 测试类中使用AOP需要手动代理 SpringBootDemoZookeeperApplicationTests target = new SpringBootDemoZookeeperApplicationTests(); AspectJProxyFactory factory = new AspectJProxyFactory(target); ZooLockAspect aspect = new ZooLockAspect(zkClient); factory.addAspect(aspect); SpringBootDemoZookeeperApplicationTests proxy = factory.getProxy(); IntStream.range(0, 10000).forEach(i -> executorService.execute(() -> proxy.aopBuy(i))); TimeUnit.MINUTES.sleep(1); log.error("count值为{}", proxy.getCount()); } /** * 测试手动加锁 */ @Test public void testManualLock() throws InterruptedException { IntStream.range(0, 10000).forEach(i -> executorService.execute(this::manualBuy)); TimeUnit.MINUTES.sleep(1); log.error("count值为{}", count); } @ZooLock(key = "buy", timeout = 1, timeUnit = TimeUnit.MINUTES) public void aopBuy(int userId) { log.info("{} 正在出库。。。", userId); doBuy(); log.info("{} 扣库存成功。。。", userId); } public void manualBuy() { String lockPath = "/buy"; log.info("try to buy sth."); try { InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath); try { if (lock.acquire(1, TimeUnit.MINUTES)) { doBuy(); log.info("buy successfully!"); } } finally { lock.release(); } } catch (Exception e) { log.error("zk error"); } } public void doBuy() { count--; log.info("count值为{}", count); } } ``` ## 参考 1. [如何在测试类中使用 AOP](https://stackoverflow.com/questions/11436600/unit-testing-spring-around-aop-methods) 2. zookeeper 实现分布式锁:《Spring Boot 2精髓 从构建小系统到架构分布式大系统》李家智 - 第16章 - Spring Boot 和 Zoo Keeper - 16.3 实现分布式锁 ================================================ FILE: demo-zookeeper/pom.xml ================================================ 4.0.0 demo-zookeeper 1.0.0-SNAPSHOT jar demo-zookeeper 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-configuration-processor true org.springframework.boot spring-boot-starter-aop org.apache.curator curator-recipes 4.1.0 cn.hutool hutool-all org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true demo-zookeeper org.springframework.boot spring-boot-maven-plugin ================================================ FILE: demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java ================================================ package com.xkcoding.zookeeper; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** *

    * 启动器 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:51 */ @SpringBootApplication public class SpringBootDemoZookeeperApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoZookeeperApplication.class, args); } } ================================================ FILE: demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/LockKeyParam.java ================================================ package com.xkcoding.zookeeper.annotation; import java.lang.annotation.*; /** *

    * 分布式锁动态key注解,配置之后key的值会动态获取参数内容 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:17 */ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface LockKeyParam { /** * 如果动态key在user对象中,那么就需要设置fields的值为user对象中的属性名可以为多个,基本类型则不需要设置该值 *

    例1:public void count(@LockKeyParam({"id"}) User user) *

    例2:public void count(@LockKeyParam({"id","userName"}) User user) *

    例3:public void count(@LockKeyParam String userId) */ String[] fields() default {}; } ================================================ FILE: demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/ZooLock.java ================================================ package com.xkcoding.zookeeper.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** *

    * 基于Zookeeper的分布式锁注解 * 在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:11 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface ZooLock { /** * 分布式锁的键 */ String key(); /** * 锁释放时间,默认五秒 */ long timeout() default 5 * 1000; /** * 时间格式,默认:毫秒 */ TimeUnit timeUnit() default TimeUnit.MILLISECONDS; } ================================================ FILE: demo-zookeeper/src/main/java/com/xkcoding/zookeeper/aspectj/ZooLockAspect.java ================================================ package com.xkcoding.zookeeper.aspectj; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.xkcoding.zookeeper.annotation.LockKeyParam; import com.xkcoding.zookeeper.annotation.ZooLock; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.locks.InterProcessMutex; import org.aspectj.lang.ProceedingJoinPoint; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; /** *

    * 使用 aop 切面记录请求日志信息 *

    * * @author yangkai.shen * @date Created in 2018-10-01 22:05 */ @Aspect @Component @Slf4j public class ZooLockAspect { private final CuratorFramework zkClient; private static final String KEY_PREFIX = "DISTRIBUTED_LOCK_"; private static final String KEY_SEPARATOR = "/"; @Autowired public ZooLockAspect(CuratorFramework zkClient) { this.zkClient = zkClient; } /** * 切入点 */ @Pointcut("@annotation(com.xkcoding.zookeeper.annotation.ZooLock)") public void doLock() { } /** * 环绕操作 * * @param point 切入点 * @return 原方法返回值 * @throws Throwable 异常信息 */ @Around("doLock()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Object[] args = point.getArgs(); ZooLock zooLock = method.getAnnotation(ZooLock.class); if (StrUtil.isBlank(zooLock.key())) { throw new RuntimeException("分布式锁键不能为空"); } String lockKey = buildLockKey(zooLock, method, args); InterProcessMutex lock = new InterProcessMutex(zkClient, lockKey); try { // 假设上锁成功,以后拿到的都是 false if (lock.acquire(zooLock.timeout(), zooLock.timeUnit())) { return point.proceed(); } else { throw new RuntimeException("请勿重复提交"); } } finally { lock.release(); } } /** * 构造分布式锁的键 * * @param lock 注解 * @param method 注解标记的方法 * @param args 方法上的参数 * @return * @throws NoSuchFieldException * @throws IllegalAccessException */ private String buildLockKey(ZooLock lock, Method method, Object[] args) throws NoSuchFieldException, IllegalAccessException { StringBuilder key = new StringBuilder(KEY_SEPARATOR + KEY_PREFIX + lock.key()); // 迭代全部参数的注解,根据使用LockKeyParam的注解的参数所在的下标,来获取args中对应下标的参数值拼接到前半部分key上 Annotation[][] parameterAnnotations = method.getParameterAnnotations(); for (int i = 0; i < parameterAnnotations.length; i++) { // 循环该参数全部注解 for (Annotation annotation : parameterAnnotations[i]) { // 注解不是 @LockKeyParam if (!annotation.annotationType().isInstance(LockKeyParam.class)) { continue; } // 获取所有fields String[] fields = ((LockKeyParam) annotation).fields(); if (ArrayUtil.isEmpty(fields)) { // 普通数据类型直接拼接 if (ObjectUtil.isNull(args[i])) { throw new RuntimeException("动态参数不能为null"); } key.append(KEY_SEPARATOR).append(args[i]); } else { // @LockKeyParam的fields值不为null,所以当前参数应该是对象类型 for (String field : fields) { Class clazz = args[i].getClass(); Field declaredField = clazz.getDeclaredField(field); declaredField.setAccessible(true); Object value = declaredField.get(clazz); key.append(KEY_SEPARATOR).append(value); } } } } return key.toString(); } } ================================================ FILE: demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/ZkConfig.java ================================================ package com.xkcoding.zookeeper.config; import com.xkcoding.zookeeper.config.props.ZkProps; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** *

    * Zookeeper配置类 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:45 */ @Configuration @EnableConfigurationProperties(ZkProps.class) public class ZkConfig { private final ZkProps zkProps; @Autowired public ZkConfig(ZkProps zkProps) { this.zkProps = zkProps; } @Bean public CuratorFramework curatorFramework() { RetryPolicy retryPolicy = new ExponentialBackoffRetry(zkProps.getTimeout(), zkProps.getRetry()); CuratorFramework client = CuratorFrameworkFactory.newClient(zkProps.getUrl(), retryPolicy); client.start(); return client; } } ================================================ FILE: demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/props/ZkProps.java ================================================ package com.xkcoding.zookeeper.config.props; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** *

    * Zookeeper 配置项 *

    * * @author yangkai.shen * @date Created in 2018-12-27 14:47 */ @Data @ConfigurationProperties(prefix = "zk") public class ZkProps { /** * 连接地址 */ private String url; /** * 超时时间(毫秒),默认1000 */ private int timeout = 1000; /** * 重试次数,默认3 */ private int retry = 3; } ================================================ FILE: demo-zookeeper/src/main/resources/application.yml ================================================ server: port: 8080 servlet: context-path: /demo zk: url: 127.0.0.1:2181 timeout: 1000 retry: 3 ================================================ FILE: demo-zookeeper/src/test/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplicationTests.java ================================================ package com.xkcoding.zookeeper; import com.xkcoding.zookeeper.annotation.ZooLock; import com.xkcoding.zookeeper.aspectj.ZooLockAspect; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.locks.InterProcessMutex; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; @RunWith(SpringRunner.class) @SpringBootTest @Slf4j public class SpringBootDemoZookeeperApplicationTests { public Integer getCount() { return count; } private Integer count = 10000; private ExecutorService executorService = Executors.newFixedThreadPool(1000); @Autowired private CuratorFramework zkClient; /** * 不使用分布式锁,程序结束查看count的值是否为0 */ @Test public void test() throws InterruptedException { IntStream.range(0, 10000).forEach(i -> executorService.execute(this::doBuy)); TimeUnit.MINUTES.sleep(1); log.error("count值为{}", count); } /** * 测试AOP分布式锁 */ @Test public void testAopLock() throws InterruptedException { // 测试类中使用AOP需要手动代理 SpringBootDemoZookeeperApplicationTests target = new SpringBootDemoZookeeperApplicationTests(); AspectJProxyFactory factory = new AspectJProxyFactory(target); ZooLockAspect aspect = new ZooLockAspect(zkClient); factory.addAspect(aspect); SpringBootDemoZookeeperApplicationTests proxy = factory.getProxy(); IntStream.range(0, 10000).forEach(i -> executorService.execute(() -> proxy.aopBuy(i))); TimeUnit.MINUTES.sleep(1); log.error("count值为{}", proxy.getCount()); } /** * 测试手动加锁 */ @Test public void testManualLock() throws InterruptedException { IntStream.range(0, 10000).forEach(i -> executorService.execute(this::manualBuy)); TimeUnit.MINUTES.sleep(1); log.error("count值为{}", count); } @ZooLock(key = "buy", timeout = 1, timeUnit = TimeUnit.MINUTES) public void aopBuy(int userId) { log.info("{} 正在出库。。。", userId); doBuy(); log.info("{} 扣库存成功。。。", userId); } public void manualBuy() { String lockPath = "/buy"; log.info("try to buy sth."); try { InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath); try { if (lock.acquire(1, TimeUnit.MINUTES)) { doBuy(); log.info("buy successfully!"); } } finally { lock.release(); } } catch (Exception e) { log.error("zk error"); } } public void doBuy() { count--; log.info("count值为{}", count); } } ================================================ FILE: jd.md ================================================ > 公司:杭州涂鸦信息科技有限公司,是一个全球云开发平台、AI+IoT开发者平台,连接消费者、制造品牌、OEM厂商和连锁零售商的智能化需求,为开发者提供一站式人工智能物联网的PaaS级解决方案。并且涵盖了硬件开发工具、全球云、智慧商业平台开发三方面,提供从技术到营销渠道的全面生态赋能,打造世界领先的IoT OS。 > > 团队:云端开发部/数据平台 > > Base:杭州 组内招人啦,HC 巨多 ~~ 感兴趣的小伙伴,简历发过来,:kissing_heart: > 微信:syk941020 > > 邮箱:237497819@qq.com > > 备注:内推+岗位 --- # 岗位列表 * [岗位列表](#岗位列表) * [高级java开发-大数据方向](#高级java开发-大数据方向) * [【职位描述】](#职位描述) * [【职位要求】](#职位要求) * [高级java大数据平台开发工程师](#高级java大数据平台开发工程师) * [【职位描述】](#职位描述-1) * [【职位要求】](#职位要求-1) * [大数据开发工程师(flink方向)](#大数据开发工程师flink方向) * [【岗位职责】](#岗位职责) * [【任职要求】](#任职要求) * [高级数据仓库开发工程师](#高级数据仓库开发工程师) * [【岗位描述】](#岗位描述) * [【技能要求】](#技能要求) * [bi分析师](#bi分析师) * [【岗位职责】](#岗位职责-1) * [【任职要求】](#任职要求-1) * [大数据平台架构师](#大数据平台架构师) * [【职位描述】](#职位描述-2) * [【职位要求】](#职位要求-2) ## 高级java开发-大数据方向 ### 【职位描述】 1. 精通Java开源框架,Java开发语言 3. 对新技术有出色的学习能力,掌握 mybatis, Spring MVC等技术 3. 参与公司大数据产品、核心架构的研发和方向预演; 4. 思维开阔喜问乐学,以提升自己的能力和效率; ### 【职位要求】 1. 精通Java语言,对相关技术领域的开源产品有深入的理解; 2. 希望你有3年以上java相关经验; 3. 熟悉Linux下的常用系统工具, 能利用工具排查CPU, 内存, IO等系统问题; 4. 从事过大规模 Web 应用开发,熟悉代码重构,性能优化,系统安全和高可用性; 5. 熟悉非关系型数据库如Redis、Hbase等。 6. 有过hbase,elasticsearch,flink,tidb,clickhouse的开发经验,对这5者有一个深入研究者优先。 7. 有过数据应用产品相关开发经验优先。 ## 高级java大数据平台开发工程师 ### 【职位描述】 1. 负责大数据平台的设计与开发实现 2. 负责大数据应用相关产品需求分析、架构设计以及开发实现 3. 负责数据产品的服务接口开发和维护 ### 【职位要求】 1. 本科及以上学历,2年及以上大数据相关技术背景 2. 熟练进行Java的代码编写,良好的代码编写素养,良好的数据结构算法技能。 3. 熟悉spring boot、mybatis、dubbo等开发框架,熟悉前后端分离开发流程 4. 有大数据平台开发经验,包括但不限于离线开发平台、数据质量中心、元数据管理、数据资产管理,实时流平台,可视化报表等 5. 熟悉开源大数据平台如HBase、ES、Kylin、tidb、clickhouse等相关技术 6. 有过使用flink做实时计算平台成功案例者和用过hera系统做过离线任务平台者优先。 ## 大数据开发工程师(flink方向) ### 【岗位职责】 1. 负责业务数据和用户行为日志的实时采集、计算、存储、服务,为业务团队提供直接数据决策; 2. 负责部门实时计算体系架构建设及实时计算平台开发改进。 3. 负责即时分析相关技术方案的探索 4. 负责实时数据仓库的建设,完善实时计算方案 ### 【任职要求】 1. 深入了解离线计算及相关开发,掌握实时计算技术体系包括数据采集、计算引擎flink等,对实时计算所涉及的事务、容错、可靠性有深入理解 并有实际项目经验; 2. 熟悉 hadoop 生态包括 hdfs/mapreduce/hive/hbase,熟悉 kafka 等实时开源工具并有项目经验; 3. 熟悉 mysql 等关系型数据库,熟悉 redis 内存数据库,熟悉 linux 系统; 4. 掌握Java或Scala语言,如并发编程和JVM等,追求高标准的工程质量; 5. 有flink实时计算开发经验,熟悉olap的相关技术。 6. 有良好的沟通能力和自我驱动动力,具备出色的规划、执行力,强烈的责任感,以及优秀的学习能力,对技术有热情,愿意不断尝试新技术和业务挑战。 ## 高级数据仓库开发工程师 ### 【岗位描述】 1. 负责数据仓库架构设计、建模和ETL开发; 2. 参与数据治理工作,提升数据易用性及数据质量; 3. 理解并合理抽象业务需求,发挥数据价值,与业务、BI团队紧密合作。 ### 【技能要求】 1. 有数据仓库需求调研和需求分析经验,能根据业务需求设计数据仓库模型,并对数据仓库数据模型进行管理,保证数据质量。 2. 精通sql开发,有较丰富的spark sql性能调优经验优先; 3. 精通数据仓库实施方法论、深入了解数据仓库体系,并支撑过实际业务场景; 4. 熟悉数据治理的相关环节、有相关开发经验或者实际应用场景; 5. 具备较强的编码能力,熟悉sql,python,hive,spark,kafka,storm中的多项; 6. 对数据敏感,认真细致,善于从数据中发现疑点; 7. 善于沟通,具备优秀的技术与业务结合能力。 ## bi分析师 ### 【岗位职责】 1. 为公司技术,运营,产品,业务策略等提供数据支持; 2. 维护,完善数据报表体系,及时,准确监控运营状况,并提供专业分析报告; 3. 通过数据来发现业务、流程中的问题、机会,从数据角度为业务部门提出相应的优化建议,并与多方合作实现流程改善,推动相关业务目标达成; 4. 沉淀分析思路与框架,提炼数据产品需求,与相关团队协作并推动数据产品的落地; ### 【任职要求】 1. 本科以上学历,2年以上工作经验,有过互联网数据分析经验者优先; 2. 扎实的数据分析、数据统计理论,善于对抽象问题进行概括; 3. 精通Excel,熟练SQL查询等操作,熟练使用至少一种数据分析工具(R、Python、SPSS等)者优先; 4. 具有良好的学习能力、沟通表达能力和团队协作能力。 ## 大数据平台架构师 ### 【职位描述】 1. 负责涂鸦大数据平台的开发建设,建立数据生态服务,解决海量数据面临的挑战 2. 参与大数据平台各类基础系统架构设计和引擎开发,集群优化,技术难点攻关 3. 集群数据安全相关体系建设,各种存储,查询方案构建 4. 协助管理、优化并维护Hadoop、Spark、flink等集群,保证集群规模持续、稳定; 5. 负责大数据产品的自动化、离线与实时计算、即席计算、数据质量、数据安全等平台的设计和开发; 6. 调研和把握当前的最新技术,将其中的先进技术引入到自己的平台中,改善产品,提升竞争力 ### 【职位要求】 1. 本科及以上学历,5年以上工作经验,3年以上大数据领域工作经验,熟悉java,spark 2. 熟悉开源大数据平台如HBase、ES、Kylin、Druid等,有实际的报表平台、多维度分析工具、etl平台、调度平台、实时平台中至少两种工具的实际建设经验。 3. 有上述相关系统为基础的实际成功的复杂系统项目的架构和开发经验 4. 热爱开源技术,熟悉一种或者多种大数据生态技术(Kafka、Hive、Hbase、Spark、Storm、Hadoop、Flink、kudu、clickhouse、tidb等),熟悉源码者优先 5. 相关开源领域的活跃贡献者或大型互联网公司相关从业经验者优先. 6. 有过使用flink做实时计算平台成功案例者和用过hera系统做过离线任务平台者优先。 ================================================ FILE: pom.xml ================================================ 4.0.0 com.xkcoding spring-boot-demo 1.0.0-SNAPSHOT demo-helloworld demo-properties demo-actuator demo-admin demo-logback demo-log-aop demo-exception-handler demo-template-freemarker demo-template-thymeleaf demo-template-beetl demo-template-enjoy demo-orm-jdbctemplate demo-orm-jpa demo-orm-mybatis demo-orm-mybatis-mapper-page demo-orm-mybatis-plus demo-orm-beetlsql demo-upload demo-cache-redis demo-cache-ehcache demo-email demo-task demo-task-quartz demo-task-xxl-job demo-swagger demo-swagger-beauty demo-rbac-security demo-rbac-shiro demo-session demo-oauth demo-social demo-zookeeper demo-mq-rabbitmq demo-mq-rocketmq demo-mq-kafka demo-websocket demo-websocket-socketio demo-ureport2 demo-uflo demo-urule demo-activiti demo-async demo-dubbo demo-war demo-elasticsearch demo-mongodb demo-neo4j demo-docker demo-multi-datasource-jpa demo-multi-datasource-mybatis demo-sharding-jdbc demo-tio demo-codegen demo-graylog demo-ldap demo-dynamic-datasource demo-ratelimit-guava demo-ratelimit-redis demo-elasticsearch-rest-high-level-client demo-https demo-flyway demo-pay pom spring-boot-demo http://xkcoding.com UTF-8 UTF-8 1.8 1.8 1.8 2.1.0.RELEASE 8.0.21 5.4.5 29.0-jre 1.20 aliyun aliyun https://maven.aliyun.com/repository/public true false org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import mysql mysql-connector-java ${mysql.version} cn.hutool hutool-all ${hutool.version} com.google.guava guava ${guava.version} eu.bitwalker UserAgentUtils ${user.agent.version} maven-clean-plugin 3.0.0 maven-resources-plugin 3.0.2 maven-compiler-plugin 3.7.0 maven-surefire-plugin 2.20.1 maven-jar-plugin 3.0.2 maven-install-plugin 2.5.2 maven-deploy-plugin 2.8.2 org.springframework.boot spring-boot-maven-plugin ${spring.boot.version} repackage