From a83769d8477210a380a791e172dc5f513a931e4c Mon Sep 17 00:00:00 2001 From: Snailclimb Date: Mon, 17 Dec 2018 15:21:46 +0800 Subject: [PATCH 001/207] =?UTF-8?q?SpringBoot=20=E6=95=B4=E5=90=88=20Rabbi?= =?UTF-8?q?tMQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- springboot-dubbo/.idea/workspace.xml | 97 ++++-- springboot-rabbitmq/.idea/compiler.xml | 16 + springboot-rabbitmq/.idea/encodings.xml | 6 + ...__ch_qos_logback_logback_classic_1_2_3.xml | 13 + ...ven__ch_qos_logback_logback_core_1_2_3.xml | 13 + .../Maven__com_fasterxml_classmate_1_4_0.xml | 13 + ...jackson_core_jackson_annotations_2_9_0.xml | 13 + ...terxml_jackson_core_jackson_core_2_9_7.xml | 13 + ...ml_jackson_core_jackson_databind_2_9_7.xml | 13 + ...n_datatype_jackson_datatype_jdk8_2_9_7.xml | 13 + ...datatype_jackson_datatype_jsr310_2_9_7.xml | 13 + ...e_jackson_module_parameter_names_2_9_7.xml | 13 + ...n__com_jayway_jsonpath_json_path_2_4_0.xml | 13 + .../Maven__com_rabbitmq_amqp_client_5_4_3.xml | 13 + ...ogle_android_json_0_0_20131108_vaadin1.xml | 13 + ..._annotation_javax_annotation_api_1_3_2.xml | 13 + ..._validation_validation_api_2_0_1_Final.xml | 13 + .../libraries/Maven__junit_junit_4_12.xml | 13 + .../Maven__net_bytebuddy_byte_buddy_1_9_5.xml | 13 + ...__net_bytebuddy_byte_buddy_agent_1_9_5.xml | 13 + ...Maven__net_minidev_accessors_smart_1_2.xml | 13 + .../Maven__net_minidev_json_smart_2_3.xml | 13 + ..._apache_logging_log4j_log4j_api_2_11_1.xml | 13 + ...he_logging_log4j_log4j_to_slf4j_2_11_1.xml | 13 + ..._tomcat_embed_tomcat_embed_core_9_0_13.xml | 13 + ...he_tomcat_embed_tomcat_embed_el_9_0_13.xml | 13 + ...at_embed_tomcat_embed_websocket_9_0_13.xml | 13 + ...Maven__org_assertj_assertj_core_3_11_1.xml | 13 + .../Maven__org_hamcrest_hamcrest_core_1_3.xml | 13 + ...ven__org_hamcrest_hamcrest_library_1_3.xml | 13 + ...dator_hibernate_validator_6_0_13_Final.xml | 13 + ...boss_logging_jboss_logging_3_3_2_Final.xml | 13 + ...Maven__org_mockito_mockito_core_2_23_4.xml | 13 + .../Maven__org_objenesis_objenesis_2_6.xml | 13 + .../Maven__org_ow2_asm_asm_5_0_4.xml | 13 + ...aven__org_skyscreamer_jsonassert_1_5_0.xml | 13 + .../Maven__org_slf4j_jul_to_slf4j_1_7_25.xml | 13 + .../Maven__org_slf4j_slf4j_api_1_7_25.xml | 13 + ...amework_amqp_spring_amqp_2_1_2_RELEASE.xml | 13 + ...ework_amqp_spring_rabbit_2_1_2_RELEASE.xml | 13 + ...amework_boot_spring_boot_2_1_1_RELEASE.xml | 13 + ...pring_boot_autoconfigure_2_1_1_RELEASE.xml | 13 + ...boot_spring_boot_starter_2_1_1_RELEASE.xml | 13 + ...spring_boot_starter_amqp_2_1_1_RELEASE.xml | 13 + ...spring_boot_starter_json_2_1_1_RELEASE.xml | 13 + ...ing_boot_starter_logging_2_1_1_RELEASE.xml | 13 + ...spring_boot_starter_test_2_1_1_RELEASE.xml | 13 + ...ring_boot_starter_tomcat_2_1_1_RELEASE.xml | 13 + ..._spring_boot_starter_web_2_1_1_RELEASE.xml | 13 + ...rk_boot_spring_boot_test_2_1_1_RELEASE.xml | 13 + ..._boot_test_autoconfigure_2_1_1_RELEASE.xml | 13 + ...ework_retry_spring_retry_1_2_2_RELEASE.xml | 13 + ...ringframework_spring_aop_5_1_3_RELEASE.xml | 13 + ...ngframework_spring_beans_5_1_3_RELEASE.xml | 13 + ...framework_spring_context_5_1_3_RELEASE.xml | 13 + ...ingframework_spring_core_5_1_3_RELEASE.xml | 13 + ...mework_spring_expression_5_1_3_RELEASE.xml | 13 + ...ringframework_spring_jcl_5_1_3_RELEASE.xml | 13 + ...amework_spring_messaging_5_1_3_RELEASE.xml | 13 + ...ingframework_spring_test_5_1_3_RELEASE.xml | 13 + ...pringframework_spring_tx_5_1_3_RELEASE.xml | 13 + ...ringframework_spring_web_5_1_3_RELEASE.xml | 13 + ...gframework_spring_webmvc_5_1_3_RELEASE.xml | 13 + .../Maven__org_xmlunit_xmlunit_core_2_6_2.xml | 13 + .../Maven__org_yaml_snakeyaml_1_23.xml | 13 + springboot-rabbitmq/.idea/misc.xml | 13 + springboot-rabbitmq/.idea/modules.xml | 8 + springboot-rabbitmq/.idea/vcs.xml | 6 + springboot-rabbitmq/.idea/workspace.xml | 242 +++++++++++++++ springboot-rabbitmq/producer/.gitignore | 25 ++ .../producer/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + springboot-rabbitmq/producer/mvnw | 286 ++++++++++++++++++ springboot-rabbitmq/producer/mvnw.cmd | 161 ++++++++++ springboot-rabbitmq/producer/pom.xml | 47 +++ .../producer/ProducerApplication.java | 14 + .../src/main/resources/application.properties | 0 .../producer/ProducerApplicationTests.java | 17 ++ 78 files changed, 1717 insertions(+), 28 deletions(-) create mode 100644 springboot-rabbitmq/.idea/compiler.xml create mode 100644 springboot-rabbitmq/.idea/encodings.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_2_3.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__ch_qos_logback_logback_core_1_2_3.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_classmate_1_4_0.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_9_0.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_9_7.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_9_7.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jdk8_2_9_7.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_jsr310_2_9_7.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_fasterxml_jackson_module_jackson_module_parameter_names_2_9_7.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_jayway_jsonpath_json_path_2_4_0.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_rabbitmq_amqp_client_5_4_3.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__com_vaadin_external_google_android_json_0_0_20131108_vaadin1.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__javax_annotation_javax_annotation_api_1_3_2.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__javax_validation_validation_api_2_0_1_Final.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__junit_junit_4_12.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__net_bytebuddy_byte_buddy_1_9_5.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__net_bytebuddy_byte_buddy_agent_1_9_5.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__net_minidev_accessors_smart_1_2.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__net_minidev_json_smart_2_3.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_apache_logging_log4j_log4j_api_2_11_1.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_apache_logging_log4j_log4j_to_slf4j_2_11_1.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_core_9_0_13.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_el_9_0_13.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_apache_tomcat_embed_tomcat_embed_websocket_9_0_13.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_assertj_assertj_core_3_11_1.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_hamcrest_hamcrest_library_1_3.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_hibernate_validator_hibernate_validator_6_0_13_Final.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_jboss_logging_jboss_logging_3_3_2_Final.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_mockito_mockito_core_2_23_4.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_objenesis_objenesis_2_6.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_ow2_asm_asm_5_0_4.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_skyscreamer_jsonassert_1_5_0.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_slf4j_jul_to_slf4j_1_7_25.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_25.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_amqp_spring_amqp_2_1_2_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_amqp_spring_rabbit_2_1_2_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_autoconfigure_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_amqp_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_json_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_logging_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_test_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_tomcat_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_starter_web_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_test_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_boot_spring_boot_test_autoconfigure_2_1_1_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_retry_spring_retry_1_2_2_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_aop_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_beans_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_context_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_core_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_expression_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_jcl_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_messaging_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_test_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_tx_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_web_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_springframework_spring_webmvc_5_1_3_RELEASE.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_xmlunit_xmlunit_core_2_6_2.xml create mode 100644 springboot-rabbitmq/.idea/libraries/Maven__org_yaml_snakeyaml_1_23.xml create mode 100644 springboot-rabbitmq/.idea/misc.xml create mode 100644 springboot-rabbitmq/.idea/modules.xml create mode 100644 springboot-rabbitmq/.idea/vcs.xml create mode 100644 springboot-rabbitmq/.idea/workspace.xml create mode 100644 springboot-rabbitmq/producer/.gitignore create mode 100644 springboot-rabbitmq/producer/.mvn/wrapper/maven-wrapper.jar create mode 100644 springboot-rabbitmq/producer/.mvn/wrapper/maven-wrapper.properties create mode 100644 springboot-rabbitmq/producer/mvnw create mode 100644 springboot-rabbitmq/producer/mvnw.cmd create mode 100644 springboot-rabbitmq/producer/pom.xml create mode 100644 springboot-rabbitmq/producer/src/main/java/top/snailclimb/producer/ProducerApplication.java create mode 100644 springboot-rabbitmq/producer/src/main/resources/application.properties create mode 100644 springboot-rabbitmq/producer/src/test/java/top/snailclimb/producer/ProducerApplicationTests.java diff --git a/springboot-dubbo/.idea/workspace.xml b/springboot-dubbo/.idea/workspace.xml index 56cf157..06ae0c4 100644 --- a/springboot-dubbo/.idea/workspace.xml +++ b/springboot-dubbo/.idea/workspace.xml @@ -3,7 +3,6 @@ - @@ -77,9 +76,12 @@ - + + + + - + @@ -108,8 +110,11 @@ - - + + + + + @@ -144,8 +149,8 @@ - - + + @@ -162,8 +167,8 @@ - - + + @@ -211,7 +216,6 @@ @@ -266,6 +271,9 @@ + + + @@ -321,6 +329,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -328,13 +360,16 @@ + + + + + + - - - - -
- - - - diff --git a/source-code/advanced/springboot-oss/src/main/resources/templates/success.html b/source-code/advanced/springboot-oss/src/main/resources/templates/success.html deleted file mode 100644 index dde1ca2..0000000 --- a/source-code/advanced/springboot-oss/src/main/resources/templates/success.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 上传结果 - - -

上传成功!

-图片地址为: - - \ No newline at end of file diff --git a/source-code/advanced/springboot-oss/src/test/java/top/snailclimb/springbootoss/SpringbootOssApplicationTests.java b/source-code/advanced/springboot-oss/src/test/java/top/snailclimb/springbootoss/SpringbootOssApplicationTests.java deleted file mode 100644 index 0737dbe..0000000 --- a/source-code/advanced/springboot-oss/src/test/java/top/snailclimb/springbootoss/SpringbootOssApplicationTests.java +++ /dev/null @@ -1,17 +0,0 @@ -package top.snailclimb.springbootoss; - -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 SpringbootOssApplicationTests { - - - @Test - public void contextLoads() { - } - -} From d7f5a5be53d459182ad6d93965997b134abc49a3 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Mon, 10 Feb 2020 23:40:27 +0800 Subject: [PATCH 135/207] =?UTF-8?q?Update=20@PostConstruct=E4=B8=8E@PreDes?= =?UTF-8?q?troy.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/basis/@PostConstruct\344\270\216@PreDestroy.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" index de506e2..d1d0eb5 100644 --- "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" +++ "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" @@ -1,4 +1,4 @@ -`@PostConstruct`和`@PreDestroy` 是两个作用于Servlet生命周期的注解,相信从 Servlet 开始学 Java 家后台开发的同学对他应该不陌生。 +`@PostConstruct`和`@PreDestroy` 是两个作用于 Servlet 生命周期的注解,相信从 Servlet 开始学 Java 后台开发的同学对他应该不陌生。 **被这两个注解修饰的方法可以保证在整个 Servlet 生命周期只被执行一次,即使 Web 容器在其内部中多次实例化该方法所在的 bean。** From 13357cc454e9afdfd665fbcf2db0744f734d55ab Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Mon, 10 Feb 2020 23:40:29 +0800 Subject: [PATCH 136/207] Update springboot-jpa-lianbiao.md --- docs/basis/springboot-jpa-lianbiao.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/basis/springboot-jpa-lianbiao.md b/docs/basis/springboot-jpa-lianbiao.md index f5535fd..0e850ab 100644 --- a/docs/basis/springboot-jpa-lianbiao.md +++ b/docs/basis/springboot-jpa-lianbiao.md @@ -158,5 +158,3 @@ List userDTOS = personRepository.filterUserInfoByAge(19,20); 1. 自定义 SQL 语句实现连表查询; 2. 自定义 SQL 语句连表查询并实现分页操作; 3. 条件查询:IN 查询,BETWEEN查询。 - -我们这一节是把 SQl 语句连表查询的逻辑放在 Dao 层直接写的,这样写的好处是比较方便,也比较简单明了。但是可能会不太好维护,很多时候我们会选择将这些逻辑放到 Service 层去做,这样也是可以实现的,下面文章我就会介绍到如何将这些写在 Dao 层的逻辑转移到 Service 层去。 \ No newline at end of file From b8f9594d970d0f46550efbdfcc58dcd972a7f3c7 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Tue, 11 Feb 2020 19:50:17 +0800 Subject: [PATCH 137/207] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5de9dcf..74d859a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ 3. **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](./docs/advanced/spring-bean-validation.md)** 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) 5. **[新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)** -6. [Spring Boot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床](./docs/advanced/springboot-oss.md) +6. **[Spring Boot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床](https://github.com/Snailclimb/springboot-aliyun-oss)** 7. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) 8. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) From 85276614631063f83718f9f4047f4666ab36300d Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Sat, 15 Feb 2020 11:26:26 +0800 Subject: [PATCH 138/207] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 74d859a..94d4959 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[推荐一下:阿里云高性能服务器,1核1g最低89,不限性能。](https://www.aliyun.com/minisite/goods?userCode=hf47liqn) +

From 2ebc45c6742588ce0d16f634df0130c194450f67 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Sun, 16 Feb 2020 14:11:40 +0800 Subject: [PATCH 139/207] =?UTF-8?q?Update=20@PostConstruct=E4=B8=8E@PreDes?= =?UTF-8?q?troy.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/basis/@PostConstruct\344\270\216@PreDestroy.md" | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" index d1d0eb5..3d76128 100644 --- "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" +++ "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" @@ -48,7 +48,7 @@ public class MyConfiguration { ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/life-cycle-annotation01.jpg) -但是 J2EE已在Java 9中弃用 `@PostConstruct`和`@PreDestroy`这两个注解 ,并计划在Java 11中将其删除。我们有什么更好的替代方法吗?当然有!而且,我比较推荐使用这种方式。 +但是 J2EE已在Java 9中弃用 `@PostConstruct`和`@PreDestroy`这两个注解 ,并计划在Java 11中将其删除。我们有什么更好的替代方法吗?当然有! ```java package cn.javaguide.config; @@ -81,6 +81,10 @@ public class MyConfiguration2 implements InitializingBean, DisposableBean { ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/life-cycle-annotation02.jpg) +但是,Spring 官方不推荐使用上面这种方式,Spring 官方文档是这样说的: + +> We recommend that you do not use the `InitializingBean` interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using the [`@PostConstruct`](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-postconstruct-and-predestroy-annotations) annotation or specifying a POJO initialization method. (我们建议您不要使用 `InitializingBean`回调接口,因为它不必要地将代码耦合到 Spring。另外,我们建议使用`@PostConstruct`注解或指定bean定义支持的通用方法。) + 如果你还是非要使用 Java 9 及以后的版本使用 `@PostConstruct`和`@PreDestroy` 这两个注解的话,你也可以手动添加相关依赖。 Maven: From 37b577def423eee8c08ad09315cfd4b858940739 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Sun, 16 Feb 2020 14:12:37 +0800 Subject: [PATCH 140/207] =?UTF-8?q?Update=20@PostConstruct=E4=B8=8E@PreDes?= =?UTF-8?q?troy.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/basis/@PostConstruct\344\270\216@PreDestroy.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" index 3d76128..6e129d7 100644 --- "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" +++ "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" @@ -81,7 +81,7 @@ public class MyConfiguration2 implements InitializingBean, DisposableBean { ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/life-cycle-annotation02.jpg) -但是,Spring 官方不推荐使用上面这种方式,Spring 官方文档是这样说的: +但是,Spring 官方不推荐使用上面这种方式,[Spring 官方文档](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-lifecycle)是这样说的: > We recommend that you do not use the `InitializingBean` interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using the [`@PostConstruct`](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-postconstruct-and-predestroy-annotations) annotation or specifying a POJO initialization method. (我们建议您不要使用 `InitializingBean`回调接口,因为它不必要地将代码耦合到 Spring。另外,我们建议使用`@PostConstruct`注解或指定bean定义支持的通用方法。) From 15ce886c07f680da4554ffdf519d82687a29ffdf Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Fri, 28 Feb 2020 11:27:18 +0800 Subject: [PATCH 141/207] =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0Kafka=E5=8E=9F?= =?UTF-8?q?=E5=88=9B=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 94d4959..f38d57f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ 4. **[Spring 如何优雅读取配置文件?](./docs/basis/read-config-properties.md)** 5. **[Spring Boot 异常处理](./docs/advanced/springboot-handle-exception.md)** 6. **[实际项目中我们是这样做异常处理的](./docs/advanced/springboot-handle-exception-plus.md)** -7. [使用 spring-boot-devtools 进行热部署](./docs/basis/spring-boot-devtools.md) +7. [使用 spring-boot-devtools 进行热部署](./docs/basis/spring-boot-devtools.md) (实际项目不太推荐热部署,影响效率) 8. **[ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md)** 9. **[JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md)** 10. [SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) @@ -45,9 +45,10 @@ 3. **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](./docs/advanced/spring-bean-validation.md)** 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) 5. **[新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)** -6. **[Spring Boot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床](https://github.com/Snailclimb/springboot-aliyun-oss)** -7. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) -8. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) +6. **[Spring Boot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床](https://github.com/Snailclimb/springboot-aliyun-oss)** +7. **[Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka)** +8. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) +9. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) ### 面试题 From 1824b91a76071763a1044ff49ad9bd009bf67584 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Sat, 7 Mar 2020 08:17:10 +0800 Subject: [PATCH 142/207] =?UTF-8?q?Update=20@PostConstruct=E4=B8=8E@PreDes?= =?UTF-8?q?troy.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/basis/@PostConstruct\344\270\216@PreDestroy.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" index 6e129d7..1e0fafb 100644 --- "a/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" +++ "b/docs/basis/@PostConstruct\344\270\216@PreDestroy.md" @@ -5,7 +5,7 @@ **这两个注解分别有什么作用呢**? 1. **`@PostConstruct`** : 用来修饰方法,标记在项目启动的时候执行这个方法,一般用来执行某些初始化操作比如全局配置。`PostConstruct` 注解的方法会在构造函数之后执行,Servlet 的`init()`方法之前执行。 -2. **`@PreDestroy`** : 当 bean 被 Web 容器的时候被调用,一般用来释放 bean 所持有的资源。。`PostConstruct` 注解的方法会在Servlet 的`destroy()`方法之前执行。 +2. **`@PreDestroy`** : 当 bean 被 Web 容器的时候被调用,一般用来释放 bean 所持有的资源。。`@PreDestroy` 注解的方法会在Servlet 的`destroy()`方法之前执行。 被这个注解修饰的方法需要满足下面这些基本条件: @@ -107,4 +107,4 @@ compile group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2 推荐阅读: -- [Spring Bean Life Cycle](https://netjs.blogspot.com/2016/03/spring-bean-life-cycle.html) \ No newline at end of file +- [Spring Bean Life Cycle](https://netjs.blogspot.com/2016/03/spring-bean-life-cycle.html) From 0bc639c153b06e9f0f3ae2b475f99ba6fcb25a32 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Sat, 7 Mar 2020 08:21:44 +0800 Subject: [PATCH 143/207] Update springboot-handle-exception-plus.md --- docs/advanced/springboot-handle-exception-plus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/springboot-handle-exception-plus.md b/docs/advanced/springboot-handle-exception-plus.md index 84030d5..5b9be96 100644 --- a/docs/advanced/springboot-handle-exception-plus.md +++ b/docs/advanced/springboot-handle-exception-plus.md @@ -267,7 +267,7 @@ public class Person { } ``` -**`ExceptionController.java`(抛出一场的类)** +**`ExceptionController.java`(抛出异常的类)** ```java @RestController From 1d35bb567442694736b906f3fff243d752627ba7 Mon Sep 17 00:00:00 2001 From: vayneXiao <576583212@qq.com> Date: Tue, 17 Mar 2020 01:07:50 +0800 Subject: [PATCH 144/207] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=862=E4=B8=AA?= =?UTF-8?q?=E7=A9=BA=E6=A0=BC=EF=BC=8C=E4=B8=BA=E4=BA=86=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E4=B8=80=E6=AC=A1pull=20request=EF=BC=8C=E8=AF=B7=E6=82=A8?= =?UTF-8?q?=E5=90=8C=E6=84=8F=E5=90=88=E5=B9=B6=E3=80=82=E8=B0=A2=E8=B0=A2?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f38d57f..4ca7f10 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,6 @@ **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) From 7ba1907d95af3fe0766fa40dedc2a8ce0b28d9cf Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 17 Mar 2020 15:56:22 +0800 Subject: [PATCH 145/207] Update springboot-jpa-lianbiao.md --- docs/basis/springboot-jpa-lianbiao.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/basis/springboot-jpa-lianbiao.md b/docs/basis/springboot-jpa-lianbiao.md index 0e850ab..8e5ac19 100644 --- a/docs/basis/springboot-jpa-lianbiao.md +++ b/docs/basis/springboot-jpa-lianbiao.md @@ -1,8 +1,8 @@ # JPA 连表查询和分页 -对于连表查询,在 JPA 中还是非常常见的,由于 JPA 可以在 respository 层自定义 SQL 语句,所以通过自定义 SQL 语句的方式实现连表还是挺简单。这篇文章是在上一篇[入门 JPA](./springboot-jpa)的文章的基础上写的,不了解 JPA 的可以先看上一篇文章。 +对于连表查询,在 JPA 中还是非常常见的,由于 JPA 可以在 respository 层自定义 SQL 语句,所以通过自定义 SQL 语句的方式实现连表还是挺简单。这篇文章是在上一篇[入门 JPA](./springboot-jpa.md)的文章的基础上写的,不了解 JPA 的可以先看上一篇文章。 -在[上一节](./springboot-jpa)的基础上我们新建了两个实体类,如下: +在[上一节](./springboot-jpa.md)的基础上我们新建了两个实体类,如下: ## 相关实体类创建 From c10517552b6e9fe4468530785d738af4f2388161 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 17 Mar 2020 15:56:54 +0800 Subject: [PATCH 146/207] Update springboot-jpa-lianbiao.md From 0f8981cbbf6c7547fe935d322ad20d3a290aa934 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Sat, 21 Mar 2020 14:05:32 +0800 Subject: [PATCH 147/207] Update springboot-handle-exception-plus.md --- docs/advanced/springboot-handle-exception-plus.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/advanced/springboot-handle-exception-plus.md b/docs/advanced/springboot-handle-exception-plus.md index 84030d5..aef67fe 100644 --- a/docs/advanced/springboot-handle-exception-plus.md +++ b/docs/advanced/springboot-handle-exception-plus.md @@ -1,4 +1,8 @@ -在上一篇文章中我介绍了: + + +> 这篇文章鸽了很久,我在这篇文章 [《用好Java中的枚举,真的没有那么简单!》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486111&idx=2&sn=8299a42edc4733bdf012ebfe9ebf3fbb&chksm=cea24554f9d5cc42eac0e0ecd851c201831f3b149e64fe11f94b548031f10ff2dac1ea9743d3&token=1729829670&lang=zh_CN#rd) 中就提到要分享。还是昨天一个读者提醒我之后,我才发现自己没有将这篇文章发到公众号。说到这里,我发现自己一个很大的问题,就是有时候在文章里面说要更新什么,结果后面就忘记了,很多时候不是自己没写,就因为各种事情混杂导致忘记发了。以后要尽量改正这个问题! + +在上一篇文章[《SpringBoot 处理异常的几种常见姿势》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485568&idx=2&sn=c5ba880fd0c5d82e39531fa42cb036ac&chksm=cea2474bf9d5ce5dcbc6a5f6580198fdce4bc92ef577579183a729cb5d1430e4994720d59b34&token=1729829670&lang=zh_CN#rd)中我介绍了: 1. 使用 `@ControllerAdvice` 和 `@ExceptionHandler` 处理全局异常 2. `@ExceptionHandler` 处理 Controller 级别的异常 From 31dfff049fc4a59ec888b31d926779c3766fadc5 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Sat, 21 Mar 2020 14:05:34 +0800 Subject: [PATCH 148/207] Create MyBloomFilter.java --- .../helloworld/controller/MyBloomFilter.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java new file mode 100644 index 0000000..7b0e691 --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java @@ -0,0 +1,90 @@ +package com.example.helloworld.controller; + +import java.util.BitSet; + +public class MyBloomFilter { + + public static void main(String[] args) { + String value1 = "https://javaguide.cn/"; + String value2 = "https://github.com/Snailclimb"; + MyBloomFilter filter = new MyBloomFilter(); + System.out.println(filter.contains(value1)); + System.out.println(filter.contains(value2)); + filter.add(value1); + filter.add(value2); + System.out.println(filter.contains(value1)); + System.out.println(filter.contains(value2)); + } + + /** + * 位数组的大小 + */ + private static final int DEFAULT_SIZE = 2 << 24; + /** + * 通过这个数组可以创建 6 个不同的哈希函数 + */ + private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; + + /** + * 位数组。数组中的元素只能是 0 或者 1 + */ + private BitSet bits = new BitSet(DEFAULT_SIZE); + + /** + * 存放包含 hash 函数的类的数组 + */ + private SimpleHash[] func = new SimpleHash[SEEDS.length]; + + /** + * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 + */ + public MyBloomFilter() { + // 初始化多个不同的 Hash 函数 + for (int i = 0; i < SEEDS.length; i++) { + func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); + } + } + + /** + * 添加元素到位数组 + */ + public void add(Object value) { + for (SimpleHash f : func) { + bits.set(f.hash(value), true); + } + } + + /** + * 判断指定元素是否存在于位数组 + */ + public boolean contains(Object value) { + boolean ret = true; + for (SimpleHash f : func) { + ret = ret && bits.get(f.hash(value)); + } + return ret; + } + + /** + * 静态内部类。用于 hash 操作! + */ + public static class SimpleHash { + + private int cap; + private int seed; + + public SimpleHash(int cap, int seed) { + this.cap = cap; + this.seed = seed; + } + + /** + * 计算 hash 值 + */ + public int hash(Object value) { + int h; + return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); + } + + } +} From a3dd8005c1fc91f5f043b1a0e9f885836dd63d2f Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Sat, 21 Mar 2020 20:08:53 +0800 Subject: [PATCH 149/207] Create CountDownLatchExample1.java --- .../controller/CountDownLatchExample1.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java new file mode 100644 index 0000000..266e38f --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java @@ -0,0 +1,44 @@ +package com.example.helloworld.controller; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author SnailClimb + * @date 2018年10月1日 + * @Description: CountDownLatch 使用方法示例 + */ +public class CountDownLatchExample1 { + // 请求的数量 + private static final int threadCount = 550; + + public static void main(String[] args) throws InterruptedException { + // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) + ExecutorService threadPool = Executors.newFixedThreadPool(300); + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount - 1; i++) { + final int threadnum = i; + threadPool.execute(() -> {// Lambda 表达式的运用 + try { + test(threadnum); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + countDownLatch.countDown();// 表示一个请求已经被完成 + } + + }); + } + countDownLatch.await(); + threadPool.shutdown(); + System.out.println("finish"); + } + + public static void test(int threadnum) throws InterruptedException { + Thread.sleep(1000);// 模拟请求的耗时操作 + System.out.println("threadnum:" + threadnum); + Thread.sleep(1000);// 模拟请求的耗时操作 + } +} From 5632a8b037edb2ad376678c9da83a139488d7500 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Fri, 3 Apr 2020 17:02:57 +0800 Subject: [PATCH 150/207] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f38d57f..e9aa199 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ 3. **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](./docs/advanced/spring-bean-validation.md)** 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) 5. **[新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)** -6. **[Spring Boot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床](https://github.com/Snailclimb/springboot-aliyun-oss)** 7. **[Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka)** 8. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) 9. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) From ccb5f015cc5437513b23f59191da2ce122f78fb8 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Thu, 7 May 2020 08:50:13 +0800 Subject: [PATCH 151/207] use h2 --- source-code/basis/jpa-demo/pom.xml | 5 ++ .../src/main/resources/application.properties | 14 ++- .../.idea/modules/webApp.main.iml | 8 ++ .../build.gradle | 2 +- .../helloworld/controller/BookController.java | 48 ---------- .../controller/CountDownLatchExample1.java | 44 --------- .../controller/HelloWorldController.java | 14 --- .../helloworld/controller/MyBloomFilter.java | 90 ------------------- .../helloworld/controller/MyRunnable.java | 36 ++++++++ .../example/helloworld/controller/Test.java | 30 +++++++ .../controller/ThreadPoolExecutorDemo.java | 38 ++++++++ .../helloworld/controller/UserController.java | 29 ++++++ .../com/example/helloworld/dto/UserDto.java | 13 +++ .../com/example/helloworld/entity/Book.java | 13 --- .../com/example/helloworld/entity/User.java | 13 +++ .../com/example/dto2entity/BeanUtilsTest.java | 30 +++++++ 16 files changed, 213 insertions(+), 214 deletions(-) create mode 100644 source-code/basis/springboot-handle-exception-improved/.idea/modules/webApp.main.iml delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/BookController.java delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/HelloWorldController.java delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/UserController.java create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/entity/Book.java create mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/entity/User.java create mode 100644 source-code/start/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java diff --git a/source-code/basis/jpa-demo/pom.xml b/source-code/basis/jpa-demo/pom.xml index 511ca9b..0bf0d4c 100644 --- a/source-code/basis/jpa-demo/pom.xml +++ b/source-code/basis/jpa-demo/pom.xml @@ -32,6 +32,11 @@ mysql-connector-java runtime + + com.h2database + h2 + runtime + org.projectlombok lombok diff --git a/source-code/basis/jpa-demo/src/main/resources/application.properties b/source-code/basis/jpa-demo/src/main/resources/application.properties index e7405ea..9d0470e 100644 --- a/source-code/basis/jpa-demo/src/main/resources/application.properties +++ b/source-code/basis/jpa-demo/src/main/resources/application.properties @@ -1,6 +1,11 @@ -spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT +# 数据库url地址 +spring.datasource.url=jdbc:h2:mem:jpa-demo spring.datasource.username=root spring.datasource.password=123456 +spring.datasource.platform=h2 +spring.datasource.driverClassName =org.h2.Driver + +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true # 打印出 sql 语句 spring.jpa.show-sql=true #常用的有四种: @@ -8,7 +13,8 @@ spring.jpa.show-sql=true # 2.create-drop:每次启动项目创建表结构,关闭项目删除表结构 # 3.update:每次启动项目会更新表结构 # 4. validate:验证表结构,不对数据库进行任何更改 -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update spring.jpa.open-in-view=false -# 创建的表的 ENGINE 为 InnoDB -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect +server.port=8080 +#H2控制台 +spring.h2.console.enabled=true diff --git a/source-code/basis/springboot-handle-exception-improved/.idea/modules/webApp.main.iml b/source-code/basis/springboot-handle-exception-improved/.idea/modules/webApp.main.iml new file mode 100644 index 0000000..4a4ce55 --- /dev/null +++ b/source-code/basis/springboot-handle-exception-improved/.idea/modules/webApp.main.iml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/source-code/basis/springboot-handle-exception-improved/build.gradle b/source-code/basis/springboot-handle-exception-improved/build.gradle index 6496f28..1f7d3f3 100644 --- a/source-code/basis/springboot-handle-exception-improved/build.gradle +++ b/source-code/basis/springboot-handle-exception-improved/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.1.9.RELEASE' + id 'org.springframework.boot' version '2.1.8.RELEASE' id 'java' } diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/BookController.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/BookController.java deleted file mode 100644 index 63e1dfa..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/BookController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.helloworld.controller; - -import com.example.helloworld.entity.Book; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -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.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@RestController -@RequestMapping(value = "/api") -public class BookController { - - private List books = new ArrayList<>(); - - @PostMapping(value = "/book") - public ResponseEntity> addBook(@RequestBody Book book) { - books.add(book); - return ResponseEntity.ok(books); - } - - @DeleteMapping("/book/{id}") - public ResponseEntity deleteBookById(@PathVariable("id") int id) { - books.remove(id); - return ResponseEntity.ok(books); - } - - @GetMapping("/book") - public ResponseEntity getBookByName(@RequestParam(value = "name") String name) { - List results = books.stream().filter(book -> book.getName().equals(name)).collect(Collectors.toList()); - return ResponseEntity.ok(results); - } - - @GetMapping("/books") - public ResponseEntity getAllBook() { - return ResponseEntity.ok(books); - } -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java deleted file mode 100644 index 266e38f..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/CountDownLatchExample1.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.helloworld.controller; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * @author SnailClimb - * @date 2018年10月1日 - * @Description: CountDownLatch 使用方法示例 - */ -public class CountDownLatchExample1 { - // 请求的数量 - private static final int threadCount = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - ExecutorService threadPool = Executors.newFixedThreadPool(300); - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - for (int i = 0; i < threadCount - 1; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 - try { - test(threadnum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - countDownLatch.countDown();// 表示一个请求已经被完成 - } - - }); - } - countDownLatch.await(); - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 - } -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/HelloWorldController.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/HelloWorldController.java deleted file mode 100644 index 90780b1..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/HelloWorldController.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.helloworld.controller; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("test") -public class HelloWorldController { - @GetMapping("hello") - public String sayHello() { - return "Hello World"; - } -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java deleted file mode 100644 index 7b0e691..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyBloomFilter.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.helloworld.controller; - -import java.util.BitSet; - -public class MyBloomFilter { - - public static void main(String[] args) { - String value1 = "https://javaguide.cn/"; - String value2 = "https://github.com/Snailclimb"; - MyBloomFilter filter = new MyBloomFilter(); - System.out.println(filter.contains(value1)); - System.out.println(filter.contains(value2)); - filter.add(value1); - filter.add(value2); - System.out.println(filter.contains(value1)); - System.out.println(filter.contains(value2)); - } - - /** - * 位数组的大小 - */ - private static final int DEFAULT_SIZE = 2 << 24; - /** - * 通过这个数组可以创建 6 个不同的哈希函数 - */ - private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; - - /** - * 位数组。数组中的元素只能是 0 或者 1 - */ - private BitSet bits = new BitSet(DEFAULT_SIZE); - - /** - * 存放包含 hash 函数的类的数组 - */ - private SimpleHash[] func = new SimpleHash[SEEDS.length]; - - /** - * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 - */ - public MyBloomFilter() { - // 初始化多个不同的 Hash 函数 - for (int i = 0; i < SEEDS.length; i++) { - func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); - } - } - - /** - * 添加元素到位数组 - */ - public void add(Object value) { - for (SimpleHash f : func) { - bits.set(f.hash(value), true); - } - } - - /** - * 判断指定元素是否存在于位数组 - */ - public boolean contains(Object value) { - boolean ret = true; - for (SimpleHash f : func) { - ret = ret && bits.get(f.hash(value)); - } - return ret; - } - - /** - * 静态内部类。用于 hash 操作! - */ - public static class SimpleHash { - - private int cap; - private int seed; - - public SimpleHash(int cap, int seed) { - this.cap = cap; - this.seed = seed; - } - - /** - * 计算 hash 值 - */ - public int hash(Object value) { - int h; - return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); - } - - } -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java new file mode 100644 index 0000000..cd7dffc --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java @@ -0,0 +1,36 @@ +package com.example.helloworld.controller; + +import java.util.Date; + +/** + * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 + * @author shuang.kou + */ +public class MyRunnable implements Runnable { + + private String command; + + public MyRunnable(String s) { + this.command = s; + } + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); + processCommand(); + System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); + } + + private void processCommand() { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public String toString() { + return this.command; + } +} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java new file mode 100644 index 0000000..6835303 --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java @@ -0,0 +1,30 @@ +package com.example.helloworld.controller; + +public class Test { + public Test() { + System.out.print("默认构造方法!--"); + } + + //非静态代码块 + { + System.out.print("非静态代码块!--"); + } + + //静态代码块 + static { + System.out.print("静态代码块!--"); + } + + private static void test() { + System.out.print("静态方法中的内容! --"); + { + System.out.print("静态方法中的代码块!--"); + } + + } + + public static void main(String[] args) { + Test test = new Test(); + Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- + } +} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java new file mode 100644 index 0000000..7f0a612 --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java @@ -0,0 +1,38 @@ +package com.example.helloworld.controller; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolExecutorDemo { + + private static final int CORE_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = 10; + private static final int QUEUE_CAPACITY = 100; + private static final Long KEEP_ALIVE_TIME = 1L; + + public static void main(String[] args) { + + //使用阿里巴巴推荐的创建线程池的方式 + //通过ThreadPoolExecutor构造函数自定义参数创建 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(QUEUE_CAPACITY), + new ThreadPoolExecutor.CallerRunsPolicy()); + + for (int i = 0; i < 10; i++) { + //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) + Runnable worker = new MyRunnable("" + i); + //执行Runnable + executor.execute(worker); + } + //终止线程池 + executor.shutdown(); + while (executor.isTerminated()) { + System.out.println("Finished all threads"); + } + } +} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/UserController.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/UserController.java new file mode 100644 index 0000000..7fc86a5 --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/UserController.java @@ -0,0 +1,29 @@ +package com.example.helloworld.controller; + +import com.example.helloworld.dto.UserDto; +import com.example.helloworld.entity.User; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +@RestController +@RequestMapping("users") +public class UserController { + + ArrayList users = new ArrayList() {{ + add(new User(11111L, "Guide哥", "koushuangbwcx@163.com", "123456")); + add(new User(22222L, "李真", "koushuangbwcx@163.com", "123456")); + add(new User(33333L, "帅", "koushuangbwcx@163.com", "123456")); + }}; + + @GetMapping + public ResponseEntity> getUsers() { + + return null; + } +} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java new file mode 100644 index 0000000..2996e18 --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java @@ -0,0 +1,13 @@ +package com.example.helloworld.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class UserDto { + String name; + String email; +} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/Book.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/Book.java deleted file mode 100644 index f4676c2..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/Book.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.helloworld.entity; - - -import lombok.Data; - -/** - * @author shuang.kou - */ -@Data -public class Book { - private String name; - private String description; -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/User.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/User.java new file mode 100644 index 0000000..f9ea15b --- /dev/null +++ b/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/User.java @@ -0,0 +1,13 @@ +package com.example.helloworld.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class User { + Long id; + String name; + String email; + String password; +} diff --git a/source-code/start/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java b/source-code/start/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java new file mode 100644 index 0000000..ef936c0 --- /dev/null +++ b/source-code/start/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java @@ -0,0 +1,30 @@ +package com.example.dto2entity; + +import com.example.helloworld.entity.User; +import com.example.helloworld.dto.UserDto; +import org.junit.Test; +import org.springframework.beans.BeanUtils; + +import static org.junit.Assert.assertFalse; +import static org.springframework.test.util.AssertionErrors.assertTrue; + +public class BeanUtilsTest { + + @Test + public void should_copy_user_properties_to_userDto() { + User user = new User(123213L, "Guide哥", "koushuangbwcx@163.com", "123456"); + UserDto userDto = new UserDto(); + BeanUtils.copyProperties(user, userDto); + assertTrue("name copied", userDto.getName().equals(user.getName())); + assertTrue("email copied", userDto.getEmail().equals(user.getEmail())); + } + + @Test + public void should_copy_user_properties_to_userDto_but_not_copy_email() { + User user = new User(123213L, "Guide哥", "koushuangbwcx@163.com", "123456"); + UserDto userDto = new UserDto(); + BeanUtils.copyProperties(user, userDto, "email"); + assertTrue("name copied", userDto.getName().equals(user.getName())); + assertTrue("email not copied", userDto.getEmail() == null); + } +} From a3a78c151554f14e88c34372035a28a2d0103e21 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Thu, 7 May 2020 08:52:10 +0800 Subject: [PATCH 152/207] Update springboot-jpa.md --- docs/basis/springboot-jpa.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/basis/springboot-jpa.md b/docs/basis/springboot-jpa.md index c3484ab..de4d8c3 100644 --- a/docs/basis/springboot-jpa.md +++ b/docs/basis/springboot-jpa.md @@ -38,6 +38,8 @@ JPA 这部分内容上手很容易,但是涉及到的东西还是挺多的, ## 2.配置数据库连接信息和JPA配置 +由于使用的是 h2 内存数据库,所以你直接运行项目数据库就会自动创建好。 + 下面的配置中需要单独说一下 `spring.jpa.hibernate.ddl-auto=create`这个配置选项。 这个属性常用的选项有四种: @@ -50,15 +52,22 @@ JPA 这部分内容上手很容易,但是涉及到的东西还是挺多的, 但是,**一定要不要在生产环境使用 ddl 自动生成表结构,一般推荐手写 SQL 语句配合 Flyway 来做这些事情。** ```properties -spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT +# 数据库url地址 +spring.datasource.url=jdbc:h2:mem:jpa-demo spring.datasource.username=root spring.datasource.password=123456 +spring.datasource.platform=h2 +spring.datasource.driverClassName =org.h2.Driver + +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true # 打印出 sql 语句 spring.jpa.show-sql=true -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update spring.jpa.open-in-view=false -# 创建的表的 ENGINE 为 InnoDB -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect +server.port=8080 +#H2控制台 +spring.h2.console.enabled=true + ``` ## 3.实体类 From c6a42a836930ddedaeb90315c5608770b5b24593 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Fri, 8 May 2020 15:31:57 +0800 Subject: [PATCH 153/207] delete unused code --- .../helloworld/controller/MyRunnable.java | 36 ------------------ .../example/helloworld/controller/Test.java | 30 --------------- .../controller/ThreadPoolExecutorDemo.java | 38 ------------------- 3 files changed, 104 deletions(-) delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java delete mode 100644 source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java deleted file mode 100644 index cd7dffc..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/MyRunnable.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.helloworld.controller; - -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java deleted file mode 100644 index 6835303..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/Test.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.helloworld.controller; - -public class Test { - public Test() { - System.out.print("默认构造方法!--"); - } - - //非静态代码块 - { - System.out.print("非静态代码块!--"); - } - - //静态代码块 - static { - System.out.print("静态代码块!--"); - } - - private static void test() { - System.out.print("静态方法中的内容! --"); - { - System.out.print("静态方法中的代码块!--"); - } - - } - - public static void main(String[] args) { - Test test = new Test(); - Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- - } -} diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java b/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java deleted file mode 100644 index 7f0a612..0000000 --- a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/ThreadPoolExecutorDemo.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.helloworld.controller; - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (executor.isTerminated()) { - System.out.println("Finished all threads"); - } - } -} From 8df7b48b6e42279b90afa7f4ae9907349b922ed8 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 2 Jun 2020 10:32:20 +0800 Subject: [PATCH 154/207] sync up with gitee --- .github/workflows/main.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..7336ffa --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +name: Sync + +on: + push: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Sync to Gitee + uses: wearerequired/wearerequired/git-mirror-action@v1 + env: + # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY + SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} + with: + # 注意替换为你的 GitHub 源仓库地址 + source-repo: "git@github.com:Snailclimb/springboot-guide.git" + # 注意替换为你的 Gitee 目标仓库地址 + destination-repo: "https://gitee.com/SnailClimb/springboot-guide.git" From 55204a5068e293f1d5462f39411a8069fd2ef3ba Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 2 Jun 2020 10:35:16 +0800 Subject: [PATCH 155/207] Update main.yml --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7336ffa..7931063 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Sync to Gitee - uses: wearerequired/wearerequired/git-mirror-action@v1 + uses: wearerequired/git-mirror-action@v1 env: - # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY - SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} + # 注意在 Settings->Secrets 配置 GITEE_SSH_PRIVATE_KEY + SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} with: # 注意替换为你的 GitHub 源仓库地址 source-repo: "git@github.com:Snailclimb/springboot-guide.git" From 3f189ca485a7d46bf1e985cf63c68bd065ad5fa6 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 2 Jun 2020 10:36:39 +0800 Subject: [PATCH 156/207] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7931063..3ff3193 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,4 +17,4 @@ jobs: # 注意替换为你的 GitHub 源仓库地址 source-repo: "git@github.com:Snailclimb/springboot-guide.git" # 注意替换为你的 Gitee 目标仓库地址 - destination-repo: "https://gitee.com/SnailClimb/springboot-guide.git" + destination-repo: "git@gitee.com:SnailClimb/springboot-guide.git" From 068c52d01ca9c308b10793ebfed2a90f3615fa4d Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Tue, 2 Jun 2020 11:03:09 +0800 Subject: [PATCH 157/207] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index e9aa199..effb95a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[推荐一下:阿里云高性能服务器,1核1g最低89,不限性能。](https://www.aliyun.com/minisite/goods?userCode=hf47liqn) -

From f2acc3df452157cdba8bc3e1e06442785ba22956 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Tue, 2 Jun 2020 11:35:08 +0800 Subject: [PATCH 158/207] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index effb95a..1e11fea 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,4 @@ **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) From 9f860953ff685cfc99024ac7f3a16764f0d6fcf0 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 2 Jun 2020 11:44:36 +0800 Subject: [PATCH 159/207] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ff3193..0cbbb43 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,6 +15,6 @@ jobs: SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} with: # 注意替换为你的 GitHub 源仓库地址 - source-repo: "git@github.com:Snailclimb/springboot-guide.git" + source-repo: "https://github.com/Snailclimb/springboot-guide.git" # 注意替换为你的 Gitee 目标仓库地址 destination-repo: "git@gitee.com:SnailClimb/springboot-guide.git" From cee64e755eb9a1615d1ac43631737f1409213de8 Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Tue, 2 Jun 2020 11:48:00 +0800 Subject: [PATCH 160/207] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e11fea..b648c4f 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,6 @@ 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! + ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) From 3504780d09ed3068674815799df647ce2d5a66b4 Mon Sep 17 00:00:00 2001 From: SnailClimb Date: Tue, 2 Jun 2020 11:50:24 +0800 Subject: [PATCH 161/207] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0cbbb43..3ff3193 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,6 +15,6 @@ jobs: SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} with: # 注意替换为你的 GitHub 源仓库地址 - source-repo: "https://github.com/Snailclimb/springboot-guide.git" + source-repo: "git@github.com:Snailclimb/springboot-guide.git" # 注意替换为你的 Gitee 目标仓库地址 destination-repo: "git@gitee.com:SnailClimb/springboot-guide.git" From 6edeaf36c62e49bfe4b6a6eedc2abac71189d39a Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Fri, 3 Jul 2020 14:16:16 +0800 Subject: [PATCH 162/207] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b648c4f..1e11fea 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,6 @@ 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - +**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) From 4a2e84e47efc0319ba025d43c886e03eebf9899b Mon Sep 17 00:00:00 2001 From: "shuang.kou" Date: Fri, 3 Jul 2020 14:17:31 +0800 Subject: [PATCH 163/207] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index c4d304e..f32ca5c 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,8 @@ 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -<<<<<<< HEAD **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! -======= + **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 ->>>>>>> 749daf315e9587a38b68b1d11a3d2eb58ae6fe9f ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) From aa07bd17834fa5853c918206a9b553047fad0eda Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 6 Aug 2020 11:04:37 +0800 Subject: [PATCH 164/207] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f32ca5c..2cb1028 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ ## 说明 1. 项目 logo 由 [logoly](https://logoly.pro/#/) 生成。 -2. 利用 docsify 生成文档部署在 Github pages: [docsify 官网介绍](https://docsify.js.org/#/) +2. 利用 docsify 生成文档部署在 Github Pages 和 Gitee Pages: [docsify 官网介绍](https://docsify.js.org/#/) ### 联系我 From 3961e3682e40270ee84b8db03a459fadaeb3e222 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 13 Oct 2020 09:59:55 +0800 Subject: [PATCH 165/207] [fix] --- docs/start/springboot-hello-world.md | 2 +- .../com/example/beanvalidationdemo/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/.DS_Store diff --git a/docs/start/springboot-hello-world.md b/docs/start/springboot-hello-world.md index 78ae5cb..2605f63 100644 --- a/docs/start/springboot-hello-world.md +++ b/docs/start/springboot-hello-world.md @@ -36,7 +36,7 @@ com +- controller | +- CustomerController.java | - | +- config + +- config | +- swagerConfig.java | ``` diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/.DS_Store b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/.DS_Store deleted file mode 100644 index feb18634b9f1e30b5d6ac09eccbbec012ac7cab1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyJ`bL3>+mcOwzbaxxbJ&N?dEfZQ z&HJ!F46k|jZL+dbKnh3!DIf);z^@hX-b8c9h8O{e zGp571j#+}(JVES*VLuSI;@5dt0!AcC>BrW{VmF2JyB5# zNP%kw?sL2F{=cUGF#lhZw37l-;9n_Vv(;v`LtnI`cs%>Sw@pkx7BSR^SZJA{Cqf From 7c6482a3205d5f720c4050cd5b732278b7fcf50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guide=E5=93=A5?= Date: Tue, 24 Nov 2020 11:43:02 +0800 Subject: [PATCH 166/207] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..169bd2d --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +javaguide.cn \ No newline at end of file From a341d9eb8f17cecc8b12551819eef995e0a69156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guide=E5=93=A5?= Date: Tue, 24 Nov 2020 11:56:12 +0800 Subject: [PATCH 167/207] Delete CNAME --- CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index 169bd2d..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -javaguide.cn \ No newline at end of file From bb3c34647ad52e888553d21bec9a8957bc8872e9 Mon Sep 17 00:00:00 2001 From: guide Date: Sat, 12 Dec 2020 20:48:50 +0800 Subject: [PATCH 168/207] =?UTF-8?q?[docs]kkFileView-=E5=9F=BA=E4=BA=8ESpri?= =?UTF-8?q?ng=20Boot=E5=9C=A8=E7=BA=BF=E6=96=87=E4=BB=B6=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- ...04\350\247\210\347\263\273\347\273\237.md" | 239 ++++++++++++++++++ 2 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 "docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" diff --git a/README.md b/README.md index 2cb1028..c81e690 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,11 @@ 8. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) 9. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) -### 面试题 +## 实战项目 + +1. [Spring Boot搭建的一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-基于Spring Boot在线文件预览系统.md) + +## 面试题 - [几道简单的 SpringBoot面试题](./docs/interview/springboot-questions.md) diff --git "a/docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" "b/docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" new file mode 100644 index 0000000..19d09a0 --- /dev/null +++ "b/docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" @@ -0,0 +1,239 @@ +> 昨晚搭建环境都花了好一会时间,主要在浪费在了安装 openoffice 这个依赖环境上(_Mac 需要手动安装_)。 +> +> 然后,又一步一步功能演示,记录,调试项目,并且简单研究了一下核心代码之后才把这篇文章写完。 +> +> 另外,这篇文章我还会简单分析一下项目核心代码。 +> +> _如果有帮助,欢迎点赞/再看鼓励,我会开心很久 ღ( ´・ᴗ・` )比心_ + +## 项目介绍 + +官方是这样介绍 kkFileView 的: + +> kkFileView 是使用 spring boot 打造文件文档在线预览项目解决方案,支持 doc、docx、ppt、pptx、xls、xlsx、zip、rar、mp4、mp3 以及众多类文本如 txt、html、xml、java、properties、sql、js、md、json、conf、ini、vue、php、py、bat、gitignore 等文件在线预览 + +**简单来说 kkFileView 就是常见的文件类型的在线预览解决方案。** + +总的来说我觉得 kkFileView 是一个非常棒的开源项目,在线文件预览这个需求非常常见。感谢开源! + +下面, 我站在一个“上帝”的角度从多个维度来评价一下 kkFileView: + +1. 代码质量一般,有很多可以优化的地方比如: + - `Controller` 层代码嵌套太多逻辑 + - 没有进行全局异常处理(_代码中是直接返回错误信息的 json 数据给前端,我并不推荐这样做_) + - 返回值不需要通过`ObjectMapper`转换为 JSON 格式(`ResponseEntity`+`@RestController` 就行了) + - ...... +2. 使用的公司比较多,说明项目整体功能还是比较稳定和成熟的! +3. 代码整体逻辑还是比较清晰的,比较容易看懂,给作者们点个赞! + +## 环境搭建 + +### 克隆项目 + +通过以下命令即可将项目克隆到本地: + +```bash +git clone https://gitee.com/kekingcn/file-online-preview.git +``` + +### 安装 OpenOffice + +office 类型的文件的预览依赖了 OpenOffice ,所以我们首先要安装 OpenOffice(Windows 下已内置,Linux 会自动安装,Mac OS 下需要手动安装)。 + +下面演示一下如何在 Mac 上安装 OpenOffice。 + +你可以通过以下命令安装最新版的 OpenOffice: + +```bash +brew cask install openoffice +``` + +不过,这种方式下载可能会比较慢,你可以直接去官网下载 dmg 安装包。 + +官方下载地址:[https://www.openoffice.org/download/](https://www.openoffice.org/download/) + +![OpenOffice下载 ](https://img-blog.csdnimg.cn/20201015194327797.png) + +很多小伙伴就要问了:**OpenOffice 是什么呢?** + +[OpenOffice](https://www.openoffice.org/) 是 Apache 旗下的一款开源免费的文字处理软件,支持 Windows、Liunx、OS X 等主流操作系统。 + +OpenOffice 和 Windows 下 office 办公软件有点类似,不过其实开源免费的。 + +![why openoffice](https://img-blog.csdnimg.cn/20201015194328447.png) + +### 启动项目 + +运行`FilePreviewApplication`的 main 方法,服务启动后,访问[http://localhost:8012/](http://localhost:8012/) 会看到如下界面,代表服务启动成功。 + +![项目启动成功](https://img-blog.csdnimg.cn/20201015194328801.png) + +## 使用 + +我们首先上传了 3 个不同的类型的文件来分别演示一下图片、PDF、Word 文档的预览。 + +![](https://img-blog.csdnimg.cn/20201015194328978.png) + +### 图片的预览 + +**kkFileView 支持 jpg,jpeg,png,gif 等多种格式图片的预览,还包括了翻转,缩放图片等操作。** + +图片的预览效果如下。 + +![图片的预览效果](https://img-blog.csdnimg.cn/20201015194329517.png) + +### Word 文档的预览 + +**kkFileView 支持 doc,docx 文档预览。** + +另外,根据 Word 大小以及网速问题, Word 预览提供了两种模式: + +- 每页 Word 转为图片预览 +- 整个 Word 文档转成 PDF,再预览 PDF。 + +两种模式的适用场景如下 + +- **图片预览** :Word 文件大(加载 PDF 速度比较慢)的情况。 +- **PDF 预览** :内网访问(加载 PDF 速度比较快)的情况。 + +图片预览模式预览效果如下: + +![](https://img-blog.csdnimg.cn/20201015194329908.png) + +PDF 预览模式预览效果如下: + +![](https://img-blog.csdnimg.cn/20201015194330591.png) + +### PDF 文档的预览 + +kkFileView 支持 PDF 文档预览。类似 Word 文档预览, PDF 预览提供了两种模式: + +- 每页 Word 转为图片预览 +- 整个 Word 文档转成 PDF,再预览 PDF。 + +由于和 Word 文档的预览展示效果一致,这里就不放图片了。 + +## 文件预览核心代码分析 + +### API 层 + +文件预览调用的接口是 `/onlinePreview` 。 + +通过分析 `/onlinePreview` 接口我们发现, 后端接收到预览请求之后,会从 URL 和请求中筛选出自己需要的信息比如文件后缀、文件名。 + +之后会调用`FilePreview`类 的 `filePreviewHandle()` 方法。`filePreviewHandle()` 方法是实现文件预览的核心方法。 + +```java +@RequestMapping(value = "/onlinePreview") +public String onlinePreview(String url, Model model, HttpServletRequest req) { + FileAttribute fileAttribute = fileUtils.getFileAttribute(url); + req.setAttribute("fileKey", req.getParameter("fileKey")); + model.addAttribute("pdfDownloadDisable", ConfigConstants.getPdfDownloadDisable()); + model.addAttribute("officePreviewType", req.getParameter("officePreviewType")); + FilePreview filePreview = previewFactory.get(fileAttribute); + logger.info("预览文件url:{},previewType:{}", url, fileAttribute.getType()); + return filePreview.filePreviewHandle(url, model, fileAttribute); +} +``` + +`FilePreview` 是文件预览接口,不同的文件类型的预览都实现了 `FilePreview` 接口,并实现了 `filePreviewHandle()` 方法。 + +### 文件预览接口 + +```java +public interface FilePreview { + String filePreviewHandle(String url, Model model, FileAttribute fileAttribute); +} +``` + +不同的文件类型的预览都实现了 `FilePreview` 接口,如下图所示。 + +![](https://img-blog.csdnimg.cn/20201015194330836.png) + +不同文件类型的预览都会实现 `FilePreview` 接口,然后重写`filePreviewHandle()`方法。比如: `OfficeFilePreviewImpl` 这个主要负责处理 office 文件的预览、`PdfFilePreviewImpl` 主要负责处理 pdf 文件的预览。 + +### 文件预览具体实现分析 + +下面我们以 office 文件的预览为入口来分析。 + +首先要明确的是 excel 类型的预览是通过将 excel 文件转换为 HTML 实现的,其他类型 office 文件的预览是通过将文件转换为 PDF 或者是 图片的方式来预览的。 + +举个例子。我们上传了一份名为 `武汉市文化市场管理办法.docx` 的 Word 文件并预览的话,`jodconverter-web/src/main/file` 路径下会生成两个相关文件,这两个文件分别对应了我们提到的 PDF 预览 和 图片预览这两种方式。 + +- `武汉市文化市场管理办法.pdf` +- 由 Word 文件所转化得到的一系列图片 + +![](https://img-blog.csdnimg.cn/2020101519433136.png) + +我们以一个名为 `武汉市文化市场管理办法.docx` 的文件来举例说明一下代码中是如何做的。 + +通过分析代码, 我们定位到了 `OfficeFilePreviewImpl` 这个主要负责处理 office 文件预览的类。 + +```java +/** + * 处理office文件 + */ +@Service +public class OfficeFilePreviewImpl implements FilePreview { + +} +``` + +我们来简单分析一下 `OfficeFilePreviewImpl` 类中实现预览的核心方法是 `filePreviewHandle` 。 + +> 说明:这部分代码的逻辑不够清晰,还可以抽方法优化以让人更容易读懂,感兴趣的小伙伴可以自己动手重构一下,然后去给作者提个 PR。 + +```java + @Override + public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { + // 1.获取预览类型(image/pdf/html),用户请求中传了officePreviewType参数就取参数的,没传取系统默认(image) + String officePreviewType = model.asMap().get("officePreviewType") == null ? ConfigConstants.getOfficePreviewType() : model.asMap().get("officePreviewType").toString(); + // 2.获取 URL 地址 + String baseUrl = BaseUrlFilter.getBaseUrl();// http://localhost:8012/ + // 3.获取图片相关信息 + String suffix=fileAttribute.getSuffix();//文件后缀如docx + String fileName=fileAttribute.getName();//文件名如:武汉市文化市场管理办法.docx + // 4. 判断是否为 html 格式预览也就是判断文件否为 excel + boolean isHtml = suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx"); + // 5. 将文件的后缀名更换为 .pdf 或者 .html(excel文件的情况) + String pdfName = fileName.substring(0, fileName.lastIndexOf(".") + 1) + (isHtml ? "html" : "pdf"); + // 6. 转换后的文件输出的文件夹如 file-online-preview/jodconverter-web/src/main/file/武汉市文化市场管理办法.pdf) + String outFilePath = FILE_DIR + pdfName; + // 7 .判断之前是否已转换过,如果转换过,直接返回,否则执行转换 + // 文件第一次被预览的时候会首先对文件进行缓存处理 + if (!fileUtils.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) { + String filePath; + // 下载文件 + ReturnResponse response = downloadUtils.downLoad(fileAttribute, null); + if (0 != response.getCode()) { + model.addAttribute("fileType", suffix); + model.addAttribute("msg", response.getMsg()); + return "fileNotSupported"; + } + filePath = response.getContent(); + if (StringUtils.hasText(outFilePath)) { + officeToPdf.openOfficeToPDF(filePath, outFilePath); + if (isHtml) { + // 对转换后的文件进行操作(改变编码方式) + fileUtils.doActionConvertedFile(outFilePath); + } + if (ConfigConstants.isCacheEnabled()) { + // 加入缓存 + fileUtils.addConvertedFile(pdfName, fileUtils.getRelativePath(outFilePath)); + } + } + } + // 8.根据预览类型officePreviewType,选择不同的预览方式 + // 比如,如果预览类型officePreviewType为pdf则进行pdf方式预览 + if (!isHtml && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) { + return getPreviewType(model, fileAttribute, officePreviewType, baseUrl, pdfName, outFilePath, pdfUtils, OFFICE_PREVIEW_TYPE_IMAGE); + } + model.addAttribute("pdfUrl", pdfName); + return isHtml ? "html" : "pdf"; + } + +``` + +项目地址: [https://github.com/kekingcn/kkFileView](https://github.com/kekingcn/kkFileView) 。 + + From 39cddbb02a741702c7fceb521e82dc9a5ec3aa28 Mon Sep 17 00:00:00 2001 From: guide Date: Sat, 12 Dec 2020 20:57:59 +0800 Subject: [PATCH 169/207] =?UTF-8?q?[docs]add=20=E4=B8=80=E4=B8=AA=20Spring?= =?UTF-8?q?Boot=20+=20Spring=20Security=20+JPA=20=E7=9A=84=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E5=88=86=E7=A6=BB=E5=90=8E=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=B3=BB=E7=BB=9F=EF=BC=81=E5=88=86=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E3=80=81RBAC=E6=9D=83=E9=99=90=E6=8E=A7?= =?UTF-8?q?=E5=88=B6...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c81e690..bb76f82 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ ## 实战项目 1. [Spring Boot搭建的一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-基于Spring Boot在线文件预览系统.md) +2. [一份热乎的 SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC权限控制...](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247495011&idx=1&sn=f574f5d75c3720d8b2a665d1d5234d28&chksm=cea1a2a8f9d62bbe9f13f5a030893fe3da6956c4be41471513e6247f74cba5a8df9941798b6e&token=212861022&lang=zh_CN#rd) ## 面试题 From bf8c12a26ecf71853339c0e76eaa4e709a237264 Mon Sep 17 00:00:00 2001 From: guide Date: Sat, 12 Dec 2020 21:25:26 +0800 Subject: [PATCH 170/207] =?UTF-8?q?[docs]=E6=8E=92=E7=89=88=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 +++++------ docs/basis/spring-boot-devtools.md | 63 ------------------- docs/start/springboot-hello-world.md | 5 -- docs/start/springboot-introduction.md | 64 ++++++++++++++------ docs/start/springboot-system-requirements.md | 30 --------- 5 files changed, 59 insertions(+), 138 deletions(-) delete mode 100644 docs/basis/spring-boot-devtools.md delete mode 100644 docs/start/springboot-system-requirements.md diff --git a/README.md b/README.md index bb76f82..2a77ba5 100644 --- a/README.md +++ b/README.md @@ -10,36 +10,24 @@ 公众号 公众号

- **在线阅读:** https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) ## 重要知识点 -### 开始 - -1. **[Spring Boot 介绍](./docs/start/springboot-introduction.md)** -2. [SpringBoot 开发环境要求](./docs/start/springboot-system-requirements.md) -3. **[Spring Boot 版 Hello World & Spring Boot 项目结构分析](./docs/start/springboot-hello-world.md)** - ### 基础 -1. **[开发 RestFul Web 服务](./docs/basis/sringboot-restful-web-service.md)** -2. [RestController VS Controller](./docs/basis/RestControllerVSController.md) -3. [`@PostConstruct`和`@PreDestroy` 简单使用以及Java9+中的替代方案](./docs/basis/@PostConstruct与@PreDestroy.md) -4. **[Spring 如何优雅读取配置文件?](./docs/basis/read-config-properties.md)** -5. **[Spring Boot 异常处理](./docs/advanced/springboot-handle-exception.md)** -6. **[实际项目中我们是这样做异常处理的](./docs/advanced/springboot-handle-exception-plus.md)** -7. [使用 spring-boot-devtools 进行热部署](./docs/basis/spring-boot-devtools.md) (实际项目不太推荐热部署,影响效率) -8. **[ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md)** -9. **[JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md)** -10. [SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) -11. [SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) -12. [整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) +1. [Spring Boot 介绍](./docs/start/springboot-introduction.md) +2. [第一个 Hello World](./docs/start/springboot-hello-world.md) +3. [第一个 RestFul Web 服务](./docs/basis/sringboot-restful-web-service.md) +4. [Spring 如何优雅读取配置文件?](./docs/basis/read-config-properties.md) +5. **异常处理** :[Spring Boot 异常处理的几种方式](./docs/advanced/springboot-handle-exception.md)、[Spring Boot 异常处理在实际项目中的应用](./docs/advanced/springboot-handle-exception-plus.md) +6. **JPA** : [ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md) 、 [JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md) +7. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) +8. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~) ### 进阶 -1. **[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md)** -2. [5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md) +1. Bean映射工具 :[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md) 、[5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md) 3. **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](./docs/advanced/spring-bean-validation.md)** 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) 5. **[新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)** @@ -47,6 +35,10 @@ 8. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) 9. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) +### 补充 + +1. [`@PostConstruct`和`@PreDestroy` 简单使用以及Java9+中的替代方案](./docs/basis/@PostConstruct与@PreDestroy.md) + ## 实战项目 1. [Spring Boot搭建的一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-基于Spring Boot在线文件预览系统.md) @@ -55,6 +47,7 @@ ## 面试题 - [几道简单的 SpringBoot面试题](./docs/interview/springboot-questions.md) +- [RestController VS Controller](./docs/basis/RestControllerVSController.md) ## 说明 diff --git a/docs/basis/spring-boot-devtools.md b/docs/basis/spring-boot-devtools.md deleted file mode 100644 index 16c910e..0000000 --- a/docs/basis/spring-boot-devtools.md +++ /dev/null @@ -1,63 +0,0 @@ -后端开发中热部署有很多方式,但是在开发 SpringBoot 项目有一种 Spring Boot 给我们提供好的很方便的一种方式,配置起来也很简单。 - -> 热部署可以简单的这样理解:我们修改程序代码后不需要重新启动程序,就可以获取到最新的代码,更新程序对外的行为。 - -热部署在我们日常开发可以为我们节省很多时间,通常我们在开发后端的过程中,当我们修改了后端代码之后都需要重启一下项目,这为我们浪费了时间,特别是在项目比较庞大,需要耗费大量时间的启动的时候。**这种方式好像消耗性能挺大的,也需要慎重使用。** - -下面介绍一下如何通过 SpringBoot 提供的 spring-boot-devtools 实现简单的热部署。 - -**依赖:** - -Maven: - -```xml - - org.springframework.boot - spring-boot-devtools - runtime - true - -``` - -```xml - - org.springframework.boot - spring-boot-maven-plugin - -``` - -Gradle: - -```groovy - -configurations { - developmentOnly - runtimeClasspath { - extendsFrom developmentOnly - } -} -dependencies { - developmentOnly("org.springframework.boot:spring-boot-devtools") -} - -``` - - - -**添加配置:** - -ctrl+,(Win) / cmd+(Mac)打开项目配置: - -输入 Compiler , 并且勾选上 Build project automatically - -![dev-tools-idea1](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/dev-tools-idea1.png) - -输入快捷键 ctrl + shift + alt + / (Win)cmd+option+shift+/(Mac),并且选择 Registry - -![dev-tools-idea2](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/dev-tools-idea2.png) - -然后勾选上 Compiler autoMake allow when app running - -![dev-tools-idea3](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/dev-tools-idea3.png) - -很简单,这样你每次修改程序之后就不用重新启动了。 \ No newline at end of file diff --git a/docs/start/springboot-hello-world.md b/docs/start/springboot-hello-world.md index 2605f63..b63c4f1 100644 --- a/docs/start/springboot-hello-world.md +++ b/docs/start/springboot-hello-world.md @@ -152,8 +152,3 @@ server.port=8333 代码地址: https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world(建议自己手敲一遍!!!) -### 代办 - -- [ ] Spring Boot 启动流程分析 - - \ No newline at end of file diff --git a/docs/start/springboot-introduction.md b/docs/start/springboot-introduction.md index 8b1e792..a6c73f6 100644 --- a/docs/start/springboot-introduction.md +++ b/docs/start/springboot-introduction.md @@ -1,43 +1,69 @@ -# 一 SpringBoot介绍 +## 1.SpringBoot 介绍 -## 1.1 先从Spring谈起 -我们知道Spring是重量级企业开发框架 **Enterprise JavaBean(EJB)** 的替代品,Spring为企业级Java开发提供了一种相对简单的方法,通过 **依赖注入** 和 **面向切面编程** ,用简单的 **Java对象(Plain Old Java Object,POJO)** 实现了EJB的功能 +### 1.1 先从 Spring 谈起 -**虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置)** 。Spring 2.5引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式XML配置。Spring 3.0引入了基于Java的配置,这是一种类型安全的可重构配置方式,可以代替XML。 +Spring 是重量级企业开发框架 **Enterprise JavaBean(EJB)** 的替代品,Spring 为企业级 Java 开发提供了一种相对简单的方法,通过 **依赖注入** 和 **面向切面编程** ,用简单的 **Java 对象(Plain Old Java Object,POJO)** 实现了 EJB 的功能 -尽管如此,我们依旧没能逃脱配置的魔爪。开启某些Spring特性时,比如事务管理和Spring MVC,还是需要用XML或Java进行显式配置。启用第三方库时也需要显式配置,比如基于Thymeleaf的Web视图。配置Servlet和过滤器(比如Spring的DispatcherServlet)同样需要在web.xml或Servlet初始化代码里进行显式配置。组件扫描减少了配置量,Java配置让它看上去简洁不少,但Spring还是需要不少配置。 +**虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的(需要大量 XML 配置)** 。 -光配置这些XML文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。 +为此,Spring 2.5 引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式 XML 配置。Spring 3.0 引入了基于 Java 的配置,这是一种类型安全的可重构配置方式,可以代替 XML。 -**不过,好消息是:Spring Boot让这一切成为了过去。** +尽管如此,我们依旧没能逃脱配置的魔爪。开启某些 Spring 特性时,比如事务管理和 Spring MVC,还是需要用 XML 或 Java 进行显式配置。启用第三方库时也需要显式配置,比如基于 Thymeleaf 的 Web 视图。配置 Servlet 和过滤器(比如 Spring 的`DispatcherServlet`)同样需要在 web.xml 或 Servlet 初始化代码里进行显式配置。组件扫描减少了配置量,Java 配置让它看上去简洁不少,但 Spring 还是需要不少配置。 -## 1.2 再来谈谈 Spring Boot +光配置这些 XML 文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。 +**不过,好消息是:Spring Boot 让这一切成为了过去。** + +### 1.2 再来谈谈 Spring Boot **最好直白的介绍莫过于官方的介绍:** -> Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”...Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) +> Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”...Most Spring Boot applications need very little Spring configuration.(Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序,只要通过 “just run”(可能是 run ‘Application’或 java -jar 或 tomcat 或 maven 插件 run 或 shell 脚本)便可以运行项目。大部分 Spring Boot 项目只需要少量的配置即可) -**简而言之,从本质上来说,Spring Boot就是Spring,它做了那些没有它你自己也会去做的Spring Bean配置。** +**简而言之,从本质上来说,Spring Boot 就是 Spring,它做了那些没有它你自己也会去做的 Spring Bean 配置。** -### 1.2.1 为什么需要 Spring Boot? +#### 1.2.1 为什么需要 Spring Boot? -Spring Framework旨在简化J2EE企业应用程序开发。Spring Boot Framework旨在简化Spring开发。 +Spring Framework 旨在简化 J2EE 企业应用程序开发。Spring Boot Framework 旨在简化 Spring 开发。 ![why-we-need-springboot](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/why-we-need-springboot.png) -### 1.2.2 Spring Boot的主要优点 +#### 1.2.2 Spring Boot 的主要优点 1. 开发基于 Spring 的应用程序很容易。 2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。 -3. Spring Boot不需要编写大量样板代码、XML配置和注释。 -4. Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。 -5. Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。 -6. Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多) -7. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。 -8. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。 +3. Spring Boot 不需要编写大量样板代码、XML 配置和注释。 +4. Spring 引导应用程序可以很容易地与 Spring 生态系统集成,如 Spring JDBC、Spring ORM、Spring Data、Spring Security 等。 +5. Spring Boot 遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。 +6. Spring Boot 应用程序提供嵌入式 HTTP 服务器,如 Tomcat 和 Jetty,可以轻松地开发和测试 web 应用程序。(这点很赞!普通运行 Java 程序的方式就能运行基于 Spring Boot web 项目,省事很多) +7. Spring Boot 提供命令行接口(CLI)工具,用于开发和测试 Spring Boot 应用程序,如 Java 或 Groovy。 +8. Spring Boot 提供了多种插件,可以使用内置工具(如 Maven 和 Gradle)开发和测试 Spring Boot 应用程序。 + +## 2.SpringBoot 开发环境要求 + +### 2.1 构建工具 + +构建工具(本项目涉及的代码大部分会采用 Maven 作为包管理工具): + +| **Build Tool** | **Version** | +| -------------- | ----------- | +| Maven | 3.3+ | +| Gradle | 4.4+ | + +### 2.2 开发工具推荐 + +推荐使用 IDEA 进行开发。最好的 Java 后台开发编辑器,没有之一! +### 2.3 Web 服务器 +Spring Boot 支持以下嵌入式 servlet 容器: +| **Name** | **Servlet Version** | +| ------------ | ------------------- | +| Tomcat 9.0 | 4.0 | +| Jetty 9.4 | 3.1 | +| Undertow 2.0 | 4.0 | +您还可以将 Spring 引导应用程序部署到任何 Servlet 3.1+兼容的 Web 容器中。 +这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。 \ No newline at end of file diff --git a/docs/start/springboot-system-requirements.md b/docs/start/springboot-system-requirements.md deleted file mode 100644 index df55803..0000000 --- a/docs/start/springboot-system-requirements.md +++ /dev/null @@ -1,30 +0,0 @@ -### JDK - -截止到目前Spring Boot 的最新版本:2.1.8.RELEASE 要求 JDK 版本在 1.8 以上,所以确保你的电脑已经正确下载安装配置了 JDK(推荐 JDK 1.8 版本)。 - -### 构建工具 - -构建工具(本项目涉及的代码大部分会采用 Maven 作为包管理工具): - -| **Build Tool** | **Version** | -| -------------- | ----------- | -| Maven | 3.3+ | -| Gradle | 4.4+ | - -### 开发工具推荐 - -推荐使用 IDEA 进行开发。最好的 Java 后台开发编辑器,没有之一! - -### Web 服务器 - -Spring Boot支持以下嵌入式servlet容器: - -| **Name** | **Servlet Version** | -| ------------ | ------------------- | -| Tomcat 9.0 | 4.0 | -| Jetty 9.4 | 3.1 | -| Undertow 2.0 | 4.0 | - -您还可以将Spring引导应用程序部署到任何Servlet 3.1+兼容的 Web 容器中。 - -这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。 \ No newline at end of file From a28a157fb2be7d7d1fe64025ca80aa413edaf870 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 15 Dec 2020 19:12:02 +0800 Subject: [PATCH 171/207] Create swagger.md --- docs/basis/swagger.md | 214 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 docs/basis/swagger.md diff --git a/docs/basis/swagger.md b/docs/basis/swagger.md new file mode 100644 index 0000000..eca3613 --- /dev/null +++ b/docs/basis/swagger.md @@ -0,0 +1,214 @@ +这篇文章,我就简单给大家聊聊项目必备的 Swagger 该怎么玩。 + +**何为 Swagger ?** 简单来说,Swagger 就是一套基于 OpenAPI 规范构建的开源工具,可以帮助我们设计、构建、记录以及使用 Rest API。 + +**为何要用 Swagger ?** 前后端分离的情况下,一份 Rest API 文档将会极大的提高我们的工作效率。前端小伙伴只需要对照着 Rest API 文档就可以搞清楚一个接口需要的参数以及返回值。通过 Swagger 我们只需要少量注解即可生成一份自带 UI 界面的 Rest API 文档,不需要我们后端手动编写。并且,通过 UI 界面,我们还可以直接对相应的 API 进行调试,省去了准备复杂的调用参数的过程。 + +这篇文章的主要内容: + +1. SpringBoot 项目中如何使用? +2. Spring Security 项目中如何使用? +3. 使用 knife4j 增强 Swagger + +以下演示所用代码,你可以在这个仓库找到:[https://github.com/Snailclimb/spring-security-jwt-guide](https://github.com/Snailclimb/spring-security-jwt-guide) (从零入门 !Spring Security With JWT(含权限验证)后端部分代码) + +## SpringBoot 项目中如何使用? + +Swagger3.0 官方已经有了自己的 Spring Boot Starter,只需要添加一个 jar 包即可(SpringBoot 版本 2.3.6.RELEASE)。。 + +```xml + + + io.springfox + springfox-boot-starter + 3.0.0 + +``` + +什么都不用配置!直接在浏览器中访问 :[http://ip:port/swagger-ui/](http://ip:port/swagger-ui/) 即可。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-08-1/swagger%E5%9C%A8SpringBoot%E4%B8%AD%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8.png) + +## Spring Security 项目中如何使用? + +如果你的项目使用了 Spring Security 做权限认证的话,你需要为 Swagger 相关 url 添加白名单。 + +```java + String[] SWAGGER_WHITELIST = { + "/swagger-ui.html", + "/swagger-ui/*", + "/swagger-resources/**", + "/v2/api-docs", + "/v3/api-docs", + "/webjars/**" + }; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors().and() + // 禁用 CSRF + .csrf().disable() + .authorizeRequests() + // swagger + .antMatchers(SWAGGER_WHITELIST).permitAll() + ...... + } +``` + +另外,某些请求需要认证之后才可以访问,为此,我们需要对 Swagger 做一些简单的配置。 + +配置的方式非常简单,我提供两种不同的方式给小伙伴们。 + +1. 登录后自动为请求添加 token。 +2. 为请求的 Header 添加一个认证参数,每次请求的时候,我们需要手动输入 token。 + +### 登录后自动为请求添加 token + +通过这种方式我们只需要授权一次即可使用所有需要授权的接口。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-15-1/%E7%99%BB%E5%BD%95%E5%90%8E%E8%87%AA%E5%8A%A8%E4%B8%BA%E8%AF%B7%E6%B1%82%E6%B7%BB%E5%8A%A0token.png) + +```java +/** + * @author shuang.kou + * @description swagger 相关配置 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("github.javaguide.springsecurityjwtguide")) + .paths(PathSelectors.any()) + .build() + .securityContexts(securityContext()) + .securitySchemes(securitySchemes()); + } + + private List securitySchemes() { + return Collections.singletonList(new ApiKey("JWT", SecurityConstants.TOKEN_HEADER, "header")); + } + + private List securityContext() { + SecurityContext securityContext = SecurityContext.builder() + .securityReferences(defaultAuth()) + .build(); + return Collections.singletonList(securityContext); + } + + List defaultAuth() { + AuthorizationScope authorizationScope + = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + return Collections.singletonList(new SecurityReference("JWT", authorizationScopes)); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Spring Security JWT Guide") + .build(); + } +} +``` + +**未登录前:** + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-15-1/%E8%87%AA%E5%8A%A8%E6%B7%BB%E5%8A%A0token-%E7%99%BB%E5%BD%95%E5%89%8D.png) + +**登录后:** + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-15-1/%E8%87%AA%E5%8A%A8%E6%B7%BB%E5%8A%A0token-%E7%99%BB%E5%BD%95%E5%90%8E.png) + +### 为请求的 Header 添加一个认证参数 + +每次请求的时候,我们需要手动输入 token 到指定位置。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-15-1/%E4%B8%BA%E8%AF%B7%E6%B1%82%E7%9A%84Header%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%AE%A4%E8%AF%81%E5%8F%82%E6%95%B0.png) + +```java +@Configuration +public class SwaggerConfig { + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("github.javaguide.springsecurityjwtguide")) + .paths(PathSelectors.any()) + .build() + .globalRequestParameters(authorizationParameter()) + .securitySchemes(securitySchemes()); + } + + private List securitySchemes() { + return Collections.singletonList(new ApiKey("JWT", SecurityConstants.TOKEN_HEADER, "header")); + } + + private List authorizationParameter() { + RequestParameterBuilder tokenBuilder = new RequestParameterBuilder(); + tokenBuilder + .name("Authorization") + .description("JWT") + .required(false) + .in("header") + .accepts(Collections.singleton(MediaType.APPLICATION_JSON)) + .build(); + return Collections.singletonList(tokenBuilder.build()); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Spring Security JWT Guide") + .build(); + } +} +``` + +## 使用 knife4j 增强 Swagger + +根据官网介绍,knife4j 是为 Java MVC 框架集成 Swagger 生成 Api 文档的增强解决方案。 + +项目地址:[https://gitee.com/xiaoym/knife4j](https://gitee.com/xiaoym/knife4j) 。 + +使用方式非常简单,添加到相关依赖即可(SpringBoot 版本 2.3.6.RELEASE)。 + +```xml + + com.github.xiaoymin + knife4j-spring-boot-starter + 3.0.2 + +``` + +完成之后,访问:[http://ip:port/doc.html](http://ip:port/doc.html) 即可。 + +效果如下。可以看出,相比于 swagger 原生 ui 确实好看实用了很多。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-08-1/image-20201211214120861.png) + +除了 UI 上的增强之外,knife4j 还提供了一些开箱即用的功能。 + +比如:**搜索 API 接口** (`knife4j` 版本>2.0.1 ) + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-08-1/image-20201211214620338.png) + +再比如:**导出离线文档** + +通过 `Knife4j` 我们可以非常方便地导出 Swagger 文档 ,并且支持多种格式。 + +> - markdown:导出当前逻辑分组下所有接口的 Markdown 格式的文档 +> - Html:导出当前逻辑分组下所有接口的 Html 格式的文档 +> - Word:导出当前逻辑分组下所有接口的 Word 格式的文档(自 2.0.5 版本开始) +> - OpenAPI:导出当前逻辑分组下的原始 OpenAPI 的规范 json 结构(自 2.0.6 版本开始) +> - PDF:未实现 + +以 HTML 格式导出的效果图如下。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-6@main/12-08-1/image-20201211215552314.png) + +还等什么?快去试试吧! \ No newline at end of file From f854e78d0be82409db9cb3f0fe935de475c303f3 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 15 Dec 2020 19:12:04 +0800 Subject: [PATCH 172/207] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2a77ba5..e0a9316 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ 6. **JPA** : [ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md) 、 [JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md) 7. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) 8. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~) +9. [SpringBoot 2.0+ 集成 Swagger 官方 Starter + knife4j 增强方案](./docs/basis/swagger.md) ### 进阶 From 6bce32babb35621c2cc224166ab09f2d165849fa Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 15 Dec 2020 19:15:43 +0800 Subject: [PATCH 173/207] Update swagger.md --- docs/basis/swagger.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/basis/swagger.md b/docs/basis/swagger.md index eca3613..3b83260 100644 --- a/docs/basis/swagger.md +++ b/docs/basis/swagger.md @@ -1,3 +1,5 @@ +> 若图片无法显示,👉:[Swagger 官方 Starter 配上这个增强方案是真的香!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247496557&idx=1&sn=77100461596999128e5d721e07f7fe3d&chksm=cea1bca6f9d635b01f45a68a6034faf76c2f5a54223b23b2dc5a0bdb15c7b7bfea0a20a76c1a&token=1835542145&lang=zh_CN#rd) + 这篇文章,我就简单给大家聊聊项目必备的 Swagger 该怎么玩。 **何为 Swagger ?** 简单来说,Swagger 就是一套基于 OpenAPI 规范构建的开源工具,可以帮助我们设计、构建、记录以及使用 Rest API。 From 6ecc9313562c0d4109b99fcb13138a747612bcc4 Mon Sep 17 00:00:00 2001 From: guide Date: Fri, 18 Dec 2020 15:58:10 +0800 Subject: [PATCH 174/207] Update springboot-hello-world.md --- docs/start/springboot-hello-world.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/start/springboot-hello-world.md b/docs/start/springboot-hello-world.md index b63c4f1..5a8d16d 100644 --- a/docs/start/springboot-hello-world.md +++ b/docs/start/springboot-hello-world.md @@ -150,5 +150,5 @@ server.port=8333 通过本文我们学到了如何新建 Spring Boot 项目、SpringBoot 项目常见的项目结构分析、`@SpringBootApplication` 注解分析,最后实现了 Spring Boot 版的 "Hello World"。 -代码地址: https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world(建议自己手敲一遍!!!) +代码地址: [https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world](https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world)(建议自己手敲一遍!!!) From 55bb3a882c5ccd6ee3fce912f90ffe829dc07eb1 Mon Sep 17 00:00:00 2001 From: guide Date: Mon, 28 Dec 2020 19:40:01 +0800 Subject: [PATCH 175/207] =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- ...ot\351\235\242\350\257\225\351\242\230.md" | 72 ++++++++++--------- 2 files changed, 41 insertions(+), 35 deletions(-) rename docs/interview/springboot-questions.md => "docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" (72%) diff --git a/README.md b/README.md index e0a9316..f2f6709 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 公众号 公众号

-**在线阅读:** https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) +**在线阅读** : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) ## 重要知识点 @@ -47,7 +47,7 @@ ## 面试题 -- [几道简单的 SpringBoot面试题](./docs/interview/springboot-questions.md) +- [几道简单的 SpringBoot面试题](./docs/interview/SpringBoot面试题.md) - [RestController VS Controller](./docs/basis/RestControllerVSController.md) ## 说明 diff --git a/docs/interview/springboot-questions.md "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" similarity index 72% rename from docs/interview/springboot-questions.md rename to "docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" index 501cb05..7061ded 100644 --- a/docs/interview/springboot-questions.md +++ "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" @@ -3,11 +3,25 @@ > - https://www.javaguides.net/2018/11/spring-boot-interview-questions-and-answers.html > - https://www.algrim.co/posts/101-spring-boot-interview-questions -### 1. 什么是 Spring Boot? +### 简单介绍一下 Spring?有啥缺点? -首先,重要的是要理解 Spring Boot 并不是一个框架,它是一种创建独立应用程序的更简单方法,只需要很少或没有配置(相比于 Spring 来说)。Spring Boot最好的特性之一是它利用现有的 Spring 项目和第三方项目来开发适合生产的应用程序。 +Spring 是重量级企业开发框架 **Enterprise JavaBean(EJB)** 的替代品,Spring 为企业级 Java 开发提供了一种相对简单的方法,通过 **依赖注入** 和 **面向切面编程** ,用简单的 **Java 对象(Plain Old Java Object,POJO)** 实现了 EJB 的功能 -### 2. 说出使用Spring Boot的主要优点 +**虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的(需要大量 XML 配置)** 。 + +为此,Spring 2.5 引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式 XML 配置。Spring 3.0 引入了基于 Java 的配置,这是一种类型安全的可重构配置方式,可以代替 XML。 + +尽管如此,我们依旧没能逃脱配置的魔爪。开启某些 Spring 特性时,比如事务管理和 Spring MVC,还是需要用 XML 或 Java 进行显式配置。启用第三方库时也需要显式配置,比如基于 Thymeleaf 的 Web 视图。配置 Servlet 和过滤器(比如 Spring 的`DispatcherServlet`)同样需要在 web.xml 或 Servlet 初始化代码里进行显式配置。组件扫描减少了配置量,Java 配置让它看上去简洁不少,但 Spring 还是需要不少配置。 + +光配置这些 XML 文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。 + +### 为什么要有SpringBoot? + +Spring 旨在简化J2EE企业应用程序开发。Spring Boot F旨在简化Spring开发(减少配置文件,开箱即用!)。 + +![why-we-need-springboot](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/why-we-need-springboot.png) + +### 说出使用Spring Boot的主要优点 1. 开发基于 Spring 的应用程序很容易。 2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。 @@ -18,15 +32,11 @@ 7. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。 8. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。 -### 3. 为什么需要Spring Boot? +### 什么是 Spring Boot Starters? -Spring Framework旨在简化J2EE企业应用程序开发。Spring Boot Framework旨在简化Spring开发。 +Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。 -![why-we-need-springboot](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/why-we-need-springboot.png) - -### 4. 什么是 Spring Boot Starters? - -Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。举个例子:在没有Spring Boot Starters之前,我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVC,Tomcat和Jackson这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。 +举个例子:在没有Spring Boot Starters之前,我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVC,Tomcat和Jackson这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。 ```xml @@ -35,9 +45,23 @@ Spring Boot Starters 是一系列依赖关系的集合,因为它的存在, ``` -### 5. 如何在Spring Boot应用程序中使用Jetty而不是Tomcat? +### Spring Boot 支持哪些内嵌 Servlet 容器? -Spring Boot Web starter使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改pom.xml(Maven)或者build.gradle(Gradle)就可以了。 +Spring Boot 支持以下嵌入式 Servlet 容器: + +| **Name** | **Servlet Version** | +| ------------ | ------------------- | +| Tomcat 9.0 | 4.0 | +| Jetty 9.4 | 3.1 | +| Undertow 2.0 | 4.0 | + +您还可以将 Spring 引导应用程序部署到任何 Servlet 3.1+兼容的 Web 容器中。 + +这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。 + +### 如何在Spring Boot应用程序中使用Jetty而不是Tomcat? + +Spring Boot (`spring-boot-starter-web`)使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改`pom.xml`(Maven)或者`build.gradle`(Gradle)就可以了。 **Maven:** @@ -71,7 +95,7 @@ compile("org.springframework.boot:spring-boot-starter-jetty") 说个题外话,从上面可以看出使用 Gradle 更加简洁明了,但是国内目前还是 Maven 使用的多一点,我个人觉得 Gradle 在很多方面都要好很多。 -### 6. 介绍一下@SpringBootApplication注解 +### 介绍一下@SpringBootApplication注解 ```java package org.springframework.boot.autoconfigure; @@ -106,7 +130,7 @@ public @interface SpringBootConfiguration { - `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的bean,注解默认会扫描该类所在的包下所有的类。 - `@Configuration`:允许在上下文中注册额外的bean或导入其他配置类 -### 7. (重要)Spring Boot 的自动配置是如何实现的? +### Spring Boot 的自动配置是如何实现的? 这个是因为`@SpringBootApplication `注解的原因,在上一个问题中已经提到了这个注解。我们知道 `@SpringBootApplication `看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan ` 注解的集合。 @@ -173,25 +197,7 @@ public class WebSecurityEnablerConfiguration { `WebSecurityEnablerConfiguration`类中使用`@ConditionalOnBean`指定了容器中必须还有`WebSecurityConfigurerAdapter` 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 `WebSecurityConfigurerAdapter`,这样自动将配置就完成了。 -更多内容可以参考这篇文章:https://sylvanassun.github.io/2018/01/08/2018-01-08-spring_boot_auto_configure/ - -### 8. Spring Boot支持哪些嵌入式web容器? - -Spring Boot支持以下嵌入式servlet容器: - -| **Name** | **Servlet Version** | -| ------------ | ------------------- | -| Tomcat 9.0 | 4.0 | -| Jetty 9.4 | 3.1 | -| Undertow 2.0 | 4.0 | - -您还可以将Spring引导应用程序部署到任何Servlet 3.1+兼容的 Web 容器中。 - -这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。 +### 开发 RESTful Web 服务常用的注解有哪些? -### 9. 什么是Spring Security ? -Spring Security 应该属于 Spring 全家桶中学习曲线比较陡峭的几个模块之一,下面我将从起源和定义这两个方面来简单介绍一下它。 -- **起源:** Spring Security 实际上起源于 Acegi Security,这个框架能为基于 Spring 的企业应用提供强大而灵活安全访问控制解决方案,并且框架这个充分利用 Spring 的 IoC 和 AOP 功能,提供声明式安全访问控制的功能。后面,随着这个项目发展, Acegi Security 成为了Spring官方子项目,后来被命名为 “Spring Security”。 -- **定义:**Spring Security 是一个功能强大且高度可以定制的框架,侧重于为Java 应用程序提供身份验证和授权。——[官方介绍](https://spring.io/projects/spring-security)。 \ No newline at end of file From 593f41b77f8cf10460005482f4726197679460ae Mon Sep 17 00:00:00 2001 From: guide Date: Mon, 28 Dec 2020 20:36:01 +0800 Subject: [PATCH 176/207] =?UTF-8?q?Update=20SpringBoot=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot\351\235\242\350\257\225\351\242\230.md" | 416 +++++++++++++++++- 1 file changed, 412 insertions(+), 4 deletions(-) diff --git "a/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" index 7061ded..d70216d 100644 --- "a/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" @@ -1,7 +1,4 @@ -> 本文由JavaGuide整理翻译自(做了适当删减、修改和补充): -> -> - https://www.javaguides.net/2018/11/spring-boot-interview-questions-and-answers.html -> - https://www.algrim.co/posts/101-spring-boot-interview-questions +市面上关于 Spring Boot 的面试题抄来抄去,简单就自己这几年使用 Spring Boot 的一些经验总结一些常见的面试题供小伙伴们自测和学习。 ### 简单介绍一下 Spring?有啥缺点? @@ -199,5 +196,416 @@ public class WebSecurityEnablerConfiguration { ### 开发 RESTful Web 服务常用的注解有哪些? +**Spring Bean相关:** + +- `@Autowired` : 自动导入对象到类中,被注入进的类同样要被 Spring 容器管理。 +- `@RestController` : `@RestController`注解是`@Controller和`@`ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。 +- `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 +- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 +- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 +- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 + +**处理常见的 HTTP 请求类型:** + +- `@GetMapping` : GET请求、 +- `@PostMapping` : POST请求。 +- `@PutMapping` : PUT请求。 +- `@DeleteMapping` : DELETE请求。 + +**前后端传值:** + +- `@RequestParam`以及`@Pathvairable :@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。` +- `@RequestBody` :用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且 Content-Type 为 `application/json` 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 + +详细介绍可以查看这篇文章:[《Spring/Spring Boot常用注解总结》](https://snailclimb.gitee.io/javaguide/#/./docs/system-design/framework/spring/SpringBoot+Spring%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3%E6%80%BB%E7%BB%93?id=_21-autowired) 。 + +### Spirng Boot 常用的两种配置文件 + +我们可以通过 `application.properties`或者 `application.yml` 对 Spring Boot 程序进行简单的配置。如果,你不进行配置的话,就是使用的默认配置。 + +### 什么是 YAML?YAML 配置的优势在哪里 ? + +YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。 + +相比于 Properties 配置的方式,YAML 配置的方式更加直观清晰,简介明了,有层次感。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/SpringBoot%E9%9D%A2%E8%AF%95%E9%A2%98/image-20201228202423406.png) + +但是,YAML 配置的方式有一个缺点,那就是不支持 `@PropertySource` 注解导入自定义的 YAML 配置。 + +### Spring Boot 常用的读取配置文件的方法有哪些? + +我们要读取的配置文件`application.yml` 内容如下: + +```yaml +wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! + +my-profile: + name: Guide哥 + email: koushuangbwcx@163.com + +library: + location: 湖北武汉加油中国加油 + books: + - name: 天才基本法 + description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 + - name: 时间的秩序 + description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 + - name: 了不起的我 + description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? +``` + +#### 通过 `@value` 读取比较简单的配置信息 + +使用 `@Value("${property}")` 读取比较简单的配置信息: + +```java +@Value("${wuhan2020}") +String wuhan2020; +``` + +> **需要注意的是 `@value`这种方式是不被推荐的,Spring 比较建议的是下面几种读取配置信息的方式。** + +#### 通过`@ConfigurationProperties`读取并与 bean 绑定 + +> **`LibraryProperties` 类上加了 `@Component` 注解,我们可以像使用普通 bean 一样将其注入到类中使用。** + +```java + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "library") +@Setter +@Getter +@ToString +class LibraryProperties { + private String location; + private List books; + + @Setter + @Getter + @ToString + static class Book { + String name; + String description; + } +} + +``` + +这个时候你就可以像使用普通 bean 一样,将其注入到类中使用: + +```java +package cn.javaguide.readconfigproperties; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author shuang.kou + */ +@SpringBootApplication +public class ReadConfigPropertiesApplication implements InitializingBean { + + private final LibraryProperties library; + + public ReadConfigPropertiesApplication(LibraryProperties library) { + this.library = library; + } + + public static void main(String[] args) { + SpringApplication.run(ReadConfigPropertiesApplication.class, args); + } + + @Override + public void afterPropertiesSet() { + System.out.println(library.getLocation()); + System.out.println(library.getBooks()); } +} +``` + +控制台输出: + +``` +湖北武汉加油中国加油 +[LibraryProperties.Book(name=天才基本法, description........] +``` + +#### 通过`@ConfigurationProperties`读取并校验 + +我们先将`application.yml`修改为如下内容,明显看出这不是一个正确的 email 格式: + +```yaml +my-profile: + name: Guide哥 + email: koushuangbwcx@ +``` + +>**`ProfileProperties` 类没有加 `@Component` 注解。我们在我们要使用`ProfileProperties` 的地方使用`@EnableConfigurationProperties`注册我们的配置bean:** + + ```java +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; + +/** + * @author shuang.kou + */ +@Getter +@Setter +@ToString +@ConfigurationProperties("my-profile") +@Validated +public class ProfileProperties { + @NotEmpty + private String name; + + @Email + @NotEmpty + private String email; + + //配置文件中没有读取到的话就用默认值 + private Boolean handsome = Boolean.TRUE; + +} + ``` + +具体使用: + +```java +package cn.javaguide.readconfigproperties; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author shuang.kou + */ +@SpringBootApplication +@EnableConfigurationProperties(ProfileProperties.class) +public class ReadConfigPropertiesApplication implements InitializingBean { + private final ProfileProperties profileProperties; + + public ReadConfigPropertiesApplication(ProfileProperties profileProperties) { + this.profileProperties = profileProperties; + } + + public static void main(String[] args) { + SpringApplication.run(ReadConfigPropertiesApplication.class, args); + } + + @Override + public void afterPropertiesSet() { + System.out.println(profileProperties.toString()); + } +} + +``` + +因为我们的邮箱格式不正确,所以程序运行的时候就报错,根本运行不起来,保证了数据类型的安全性: + +```visual basic +Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my-profile' to cn.javaguide.readconfigproperties.ProfileProperties failed: + + Property: my-profile.email + Value: koushuangbwcx@ + Origin: class path resource [application.yml]:5:10 + Reason: must be a well-formed email address +``` + +我们把邮箱测试改为正确的之后再运行,控制台就能成功打印出读取到的信息: + +``` +ProfileProperties(name=Guide哥, email=koushuangbwcx@163.com, handsome=true) +``` + +#### `@PropertySource`读取指定的 properties 文件 + +```java +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +@Component +@PropertySource("classpath:website.properties") +@Getter +@Setter +class WebSite { + @Value("${url}") + private String url; +} +``` + +使用: + +```java +@Autowired +private WebSite webSite; + +System.out.println(webSite.getUrl());//https://javaguide.cn/ + +``` + +### Spring Boot加载配置文件的优先级了解么? + +Spring 读取配置文件也是有优先级的,直接上图: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/read-config-properties-priority.jpg) + +更对内容请查看官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config + +### 常用的 Bean 映射工具有哪些? + + MapStruct、ModelMapper、Dozer、Orika、JMapper是 5 种常用的 Bean 映射工具。 + +综合日常使用情况和相关测试数据,个人感觉 MapStruct、ModelMapper 这两个 Bean 映射框架是最佳选择。 + +### Spring Boot 如何做请求参数校验? + +数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。 + +Spring Boot 程序做请求参数校验的话只需要`spring-boot-starter-web` 依赖就够了,它的子依赖包含了我们所需要的东西。 + +#### 校验注解 + +**JSR提供的校验注解**: + +- `@Null` 被注释的元素必须为 null +- `@NotNull` 被注释的元素必须不为 null +- `@AssertTrue` 被注释的元素必须为 true +- `@AssertFalse` 被注释的元素必须为 false +- `@Min(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 +- `@Max(value) ` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 +- `@DecimalMin(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 +- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 +- `@Size(max=, min=) ` 被注释的元素的大小必须在指定的范围内 +- `@Digits (integer, fraction) ` 被注释的元素必须是一个数字,其值必须在可接受的范围内 +- `@Past ` 被注释的元素必须是一个过去的日期 +- `@Future` 被注释的元素必须是一个将来的日期 +- `@Pattern(regex=,flag=) ` 被注释的元素必须符合指定的正则表达式 + +**Hibernate Validator提供的校验注解**: + +- `@NotBlank(message =) ` 验证字符串非null,且长度必须大于0 +- `@Email` 被注释的元素必须是电子邮箱地址 +- `@Length(min=,max=) ` 被注释的字符串的大小必须在指定的范围内 +- `@NotEmpty ` 被注释的字符串的必须非空 +- `@Range(min=,max=,message=)` 被注释的元素必须在合适的范围内 + +**使用示例:** + +```java +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Person { + + @NotNull(message = "classId 不能为空") + private String classId; + + @Size(max = 33) + @NotNull(message = "name 不能为空") + private String name; + + @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围") + @NotNull(message = "sex 不能为空") + private String sex; + + @Email(message = "email 格式不正确") + @NotNull(message = "email 不能为空") + private String email; + +} +``` + +#### 验证请求体(RequestBody) + +我们在需要验证的参数上加上了`@Valid` 注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。 + +```java +@RestController +@RequestMapping("/api") +public class PersonController { + + @PostMapping("/person") + public ResponseEntity getPerson(@RequestBody @Valid Person person) { + return ResponseEntity.ok().body(person); + } +} +``` + +#### 验证请求参数(Path Variables 和 Request Parameters) + +一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。 + +```java +@RestController +@RequestMapping("/api") +@Validated +public class PersonController { + + @GetMapping("/person/{id}") + public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { + return ResponseEntity.ok().body(id); + } + + @PutMapping("/person") + public ResponseEntity getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) { + return ResponseEntity.ok().body(name); + } +} +``` + +更多内容请参考我的原创: **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://snailclimb.gitee.io/springboot-guide/#/./docs/advanced/spring-bean-validation)** + +### 如何使用 Spring Boot 实现全局异常处理? + +可以使用 `@ControllerAdvice` 和 `@ExceptionHandler` 处理全局异常。 + +更多内容请参考我的原创 :[Spring Boot 异常处理在实际项目中的应用](https://snailclimb.gitee.io/springboot-guide/#/./docs/advanced/springboot-handle-exception-plus) + +### Spring Boot 中如何实现定时任务 ? + +我们使用 `@Scheduled` 注解就能很方便地创建一个定时任务。 + +```java +@Component +public class ScheduledTasks { + private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); + + /** + * fixedRate:固定速率执行。每5秒执行一次。 + */ + @Scheduled(fixedRate = 5000) + public void reportCurrentTimeWithFixedRate() { + log.info("Current Thread : {}", Thread.currentThread().getName()); + log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date())); + } +} +``` + +单纯依靠 `@Scheduled` 注解 还不行,我们还需要在 SpringBoot 中我们只需要在启动类上加上`@EnableScheduling` 注解,这样才可以启动定时任务。`@EnableScheduling` 注解的作用是发现注解 `@Scheduled` 的任务并在后台执行该任务。 + From 4dc57d62d8d770ded1f15bf36cc3374140d2b35c Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 29 Dec 2020 15:23:04 +0800 Subject: [PATCH 177/207] delete workflow --- .DS_Store | Bin 0 -> 6148 bytes .github/.DS_Store | Bin 0 -> 6148 bytes .github/workflows/main.yml | 20 -------------------- 3 files changed, 20 deletions(-) create mode 100644 .DS_Store create mode 100644 .github/.DS_Store delete mode 100644 .github/workflows/main.yml diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e848d82458ef7b90e87b4b72e9c66496134514de GIT binary patch literal 6148 zcmeHK%}N6?5T3ME(~8)GpvPRi6jdqYYHNS#bKj~Dc6@_)QAU_xF0oodK}LL zq|D2Ft++lM9v+m`?w&I$sbRV7l+?bnGaBXP`sVi0Y5nfuariWPelgSv{0p_LS{%bG z8edj(zi z*GG!y2uaYUw*;ZG=vqt-VgyB)QbbcK>=Q$na`Y>k=UPk+nsN|&W*o=PEbI$K=-JV) zbUFyvAh*l_Gce6S)^v+>{-6B({+}-59y7oU%oPJ7SM{nDT#`LoXBJ0itwOD%l2Bf1 ma8iPXK8i7yj^YheE$CNdAi5S)gXlrwi-4wq8)o278TbHe22v{k literal 0 HcmV?d00001 diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c29fa0aac579c0be056cac352053dbf583864b9c GIT binary patch literal 6148 zcmeHK&5qMB5FWQ>*Xasv4@l+c3%4FpK$JbK6lKeS+e$%j0MsUGmqvtm)ubD--J(1O zywSb^ufvS(R#Yt)4hU5<()b(Ck1c=*zG3Da-F3`>H9xLquF|JbS!0CwjZ`X zMl)SSWn3lisM)T`tdF_SCcI{;H>zgMr^uFZ`#gl_$GT`sc65U%uWh?^fTwTY3e4 ziAGKxuHgq7dsccA&GSs>pD?H>LP%m6d+*9_44pil_| zhqXm>bYMfjj}&halAuj*2}0#Ca9CT!2#T`bP$0>ZkYjQV4H!a?RMz?fA#10|8^1gm;q+spcoLXVKfYJN$zglSsdN9 u3U!Q1LV2~tO$i$2D#l#8iXWpILBApcF>qL0L=Ou82pAf;VFvyw1HS;AXj9|> literal 0 HcmV?d00001 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 3ff3193..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Sync - -on: - push: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Sync to Gitee - uses: wearerequired/git-mirror-action@v1 - env: - # 注意在 Settings->Secrets 配置 GITEE_SSH_PRIVATE_KEY - SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} - with: - # 注意替换为你的 GitHub 源仓库地址 - source-repo: "git@github.com:Snailclimb/springboot-guide.git" - # 注意替换为你的 Gitee 目标仓库地址 - destination-repo: "git@gitee.com:SnailClimb/springboot-guide.git" From 2c94999fa8cd5dd88941b525d43e277c45123658 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 29 Dec 2020 15:24:13 +0800 Subject: [PATCH 178/207] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98~?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f2f6709..0c73bb8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ 公众号 公众号

+ **在线阅读** : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) ## 重要知识点 From 3d9d58a47354e10663730b6fc003ad14e8a34585 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 31 Dec 2020 14:12:16 +0800 Subject: [PATCH 179/207] =?UTF-8?q?[init]=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-generator-freemarker/.gitignore | 33 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 118 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../practice/code-generator-freemarker/mvnw | 322 ++++++++++++++++++ .../code-generator-freemarker/mvnw.cmd | 182 ++++++++++ .../code-generator-freemarker/pom.xml | 65 ++++ .../CodeGeneratorFreemarkerApplication.java | 13 + .../javaguide/rest/common/ResponseResult.java | 58 ++++ .../cn/javaguide/rest/common/ResultCode.java | 21 ++ .../controller/CodeGeneratorController.java | 15 + .../service/CodeGeneratorService.java | 11 + .../main/java/cn/javaguide/util/DbUtil.java | 27 ++ .../src/main/resources/application.properties | 1 + ...deGeneratorFreemarkerApplicationTests.java | 13 + 15 files changed, 881 insertions(+) create mode 100644 source-code/practice/code-generator-freemarker/.gitignore create mode 100644 source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.jar create mode 100644 source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties create mode 100755 source-code/practice/code-generator-freemarker/mvnw create mode 100644 source-code/practice/code-generator-freemarker/mvnw.cmd create mode 100644 source-code/practice/code-generator-freemarker/pom.xml create mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java create mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java create mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java create mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java create mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java create mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java create mode 100644 source-code/practice/code-generator-freemarker/src/main/resources/application.properties create mode 100644 source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java diff --git a/source-code/practice/code-generator-freemarker/.gitignore b/source-code/practice/code-generator-freemarker/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/source-code/practice/code-generator-freemarker/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### 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/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java b/source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.jar b/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties b/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/source-code/practice/code-generator-freemarker/mvnw b/source-code/practice/code-generator-freemarker/mvnw new file mode 100755 index 0000000..3c8a553 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/mvnw @@ -0,0 +1,322 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="$(/usr/libexec/java_home)" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +if [ -z "$M2_HOME" ]; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then + PRG="$link" + else + PRG="$(dirname "$PRG")/$link" + fi + done + + saveddir=$(pwd) + + M2_HOME=$(dirname "$PRG")/.. + + # make it fully qualified + M2_HOME=$(cd "$M2_HOME" && pwd) + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=$(cygpath --unix "$M2_HOME") + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$M2_HOME" ] && + M2_HOME="$( ( + cd "$M2_HOME" + pwd + ))" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="$( ( + cd "$JAVA_HOME" + pwd + ))" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr \"$javaExecutable\" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! $(expr "$readLink" : '\([^ ]*\)') = "no" ]; then + if $darwin; then + javaHome="$(dirname \"$javaExecutable\")" + javaExecutable="$(cd \"$javaHome\" && pwd -P)/javac" + else + javaExecutable="$(readlink -f \"$javaExecutable\")" + fi + javaHome="$(dirname \"$javaExecutable\")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(which java)" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." + pwd + ) + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' <"$1")" + fi +} + +BASE_DIR=$(find_maven_basedir "$(pwd)") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in wrapperUrl) + jarUrl="$value" + break + ;; + esac + done <"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=$(cygpath --path --windows "$M2_HOME") + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/source-code/practice/code-generator-freemarker/mvnw.cmd b/source-code/practice/code-generator-freemarker/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/source-code/practice/code-generator-freemarker/pom.xml b/source-code/practice/code-generator-freemarker/pom.xml new file mode 100644 index 0000000..f0e5a0a --- /dev/null +++ b/source-code/practice/code-generator-freemarker/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.1 + + + cn.javaguide + code-generator-freemarker + 0.0.1-SNAPSHOT + code-generator-freemarker + Demo project for Spring Boot + + + 8 + + 1.7.25 + + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + org.springframework.boot + spring-boot-starter-web + + + mysql + mysql-connector-java + runtime + + + org.projectlombok + lombok + 1.18.16 + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java new file mode 100644 index 0000000..17381f4 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java @@ -0,0 +1,13 @@ +package cn.javaguide; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CodeGeneratorFreemarkerApplication { + + public static void main(String[] args) { + SpringApplication.run(CodeGeneratorFreemarkerApplication.class, args); + } + +} \ No newline at end of file diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java new file mode 100644 index 0000000..3dd1ad6 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java @@ -0,0 +1,58 @@ +package cn.javaguide.rest.common; + + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ResponseResult { + /** + * 状态码 + */ + private Integer status; + /** + * 提示信息 + */ + private String msg; + /** + * 数据封装 + */ + private T obj; + + protected ResponseResult(Integer status, String msg, T obj) { + this.status = status; + this.msg = msg; + this.obj = obj; + } + + public static ResponseResult success(T data) { + return new ResponseResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + public static ResponseResult success(String message) { + return new ResponseResult(ResultCode.SUCCESS.getCode(), message, null); + } + + public static ResponseResult success(String message, T data) { + return new ResponseResult(ResultCode.SUCCESS.getCode(), message, data); + } + + + public static ResponseResult failed(ResultCode errorCode) { + return new ResponseResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + public static ResponseResult failed(ResultCode errorCode, String message) { + return new ResponseResult(errorCode.getCode(), message, null); + } + + public static ResponseResult failed(String message) { + return new ResponseResult(ResultCode.FAILED.getCode(), message, null); + } + + public static ResponseResult failed() { + return failed(ResultCode.FAILED); + } + +} \ No newline at end of file diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java new file mode 100644 index 0000000..1037ae0 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java @@ -0,0 +1,21 @@ +package cn.javaguide.rest.common; + +public enum ResultCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"); + private Integer code; + private String message; + + ResultCode(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java new file mode 100644 index 0000000..afe376c --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java @@ -0,0 +1,15 @@ +package cn.javaguide.rest.controller; + +import cn.javaguide.service.CodeGeneratorService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author shuang.kou + * @createTime 2020年12月31日 14:03:00 + **/ +@RestController +@RequiredArgsConstructor +public class CodeGeneratorController { + private final CodeGeneratorService codeGeneratorService; +} diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java new file mode 100644 index 0000000..983ec64 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java @@ -0,0 +1,11 @@ +package cn.javaguide.service; + +import org.springframework.stereotype.Service; + +/** + * @author shuang.kou + * @createTime 2020年12月31日 14:03:00 + **/ +@Service +public class CodeGeneratorService { +} diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java new file mode 100644 index 0000000..221eb38 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java @@ -0,0 +1,27 @@ +package cn.javaguide.util; + +import lombok.extern.slf4j.Slf4j; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +@Slf4j +public class DbUtil { + public static final String MYSQL_JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; + private static Connection connection; + + public static Connection getConnection() { + return connection; + } + + public static Connection getConnection(String url, String username, String password) throws ClassNotFoundException, SQLException { + if (connection == null) { + Class.forName(MYSQL_JDBC_DRIVER); + log.info("MySQL 数据库驱动加载成功"); + connection = DriverManager.getConnection(url, username, password); + } + return connection; + } + +} diff --git a/source-code/practice/code-generator-freemarker/src/main/resources/application.properties b/source-code/practice/code-generator-freemarker/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java b/source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java new file mode 100644 index 0000000..ef2f168 --- /dev/null +++ b/source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java @@ -0,0 +1,13 @@ +package cn.javaguide; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CodeGeneratorFreemarkerApplicationTests { + + @Test + void contextLoads() { + } + +} From db158661674e24e65501a1c62a5f5d7f8aa16e87 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 31 Dec 2020 14:14:16 +0800 Subject: [PATCH 180/207] =?UTF-8?q?Update=20SpringBoot=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot\351\235\242\350\257\225\351\242\230.md" | 180 ++++++++++-------- 1 file changed, 97 insertions(+), 83 deletions(-) diff --git "a/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" index d70216d..05f6eba 100644 --- "a/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" @@ -1,6 +1,8 @@ -市面上关于 Spring Boot 的面试题抄来抄去,简单就自己这几年使用 Spring Boot 的一些经验总结一些常见的面试题供小伙伴们自测和学习。 +市面上关于 Spring Boot 的面试题抄来抄去,毫无价值可言。 -### 简单介绍一下 Spring?有啥缺点? +这篇文章,我会简单就自己这几年使用 Spring Boot 的一些经验,总结一些常见的面试题供小伙伴们自测和学习。少部分关于 Spring/Spring Boot 的介绍参考了官网,其他皆为原创。 + +### 1. 简单介绍一下 Spring?有啥缺点? Spring 是重量级企业开发框架 **Enterprise JavaBean(EJB)** 的替代品,Spring 为企业级 Java 开发提供了一种相对简单的方法,通过 **依赖注入** 和 **面向切面编程** ,用简单的 **Java 对象(Plain Old Java Object,POJO)** 实现了 EJB 的功能 @@ -12,28 +14,28 @@ Spring 是重量级企业开发框架 **Enterprise JavaBean(EJB)** 的替代 光配置这些 XML 文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。 -### 为什么要有SpringBoot? +### 2. 为什么要有 SpringBoot? -Spring 旨在简化J2EE企业应用程序开发。Spring Boot F旨在简化Spring开发(减少配置文件,开箱即用!)。 +Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot F 旨在简化 Spring 开发(减少配置文件,开箱即用!)。 ![why-we-need-springboot](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/why-we-need-springboot.png) -### 说出使用Spring Boot的主要优点 +### 3. 说出使用 Spring Boot 的主要优点 1. 开发基于 Spring 的应用程序很容易。 2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。 -3. Spring Boot不需要编写大量样板代码、XML配置和注释。 -4. Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。 -5. Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。 -6. Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多) -7. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。 -8. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。 +3. Spring Boot 不需要编写大量样板代码、XML 配置和注释。 +4. Spring 引导应用程序可以很容易地与 Spring 生态系统集成,如 Spring JDBC、Spring ORM、Spring Data、Spring Security 等。 +5. Spring Boot 遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。 +6. Spring Boot 应用程序提供嵌入式 HTTP 服务器,如 Tomcat 和 Jetty,可以轻松地开发和测试 web 应用程序。(这点很赞!普通运行 Java 程序的方式就能运行基于 Spring Boot web 项目,省事很多) +7. Spring Boot 提供命令行接口(CLI)工具,用于开发和测试 Spring Boot 应用程序,如 Java 或 Groovy。 +8. Spring Boot 提供了多种插件,可以使用内置工具(如 Maven 和 Gradle)开发和测试 Spring Boot 应用程序。 -### 什么是 Spring Boot Starters? +### 4. 什么是 Spring Boot Starters? Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。 -举个例子:在没有Spring Boot Starters之前,我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVC,Tomcat和Jackson这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。 +举个例子:在没有 Spring Boot Starters 之前,我们开发 REST 服务或 Web 应用程序时; 我们需要使用像 Spring MVC,Tomcat 和 Jackson 这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发 REST 服务需要的所有依赖。 ```xml @@ -42,7 +44,7 @@ Spring Boot Starters 是一系列依赖关系的集合,因为它的存在, ``` -### Spring Boot 支持哪些内嵌 Servlet 容器? +### 5. Spring Boot 支持哪些内嵌 Servlet 容器? Spring Boot 支持以下嵌入式 Servlet 容器: @@ -56,9 +58,9 @@ Spring Boot 支持以下嵌入式 Servlet 容器: 这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。 -### 如何在Spring Boot应用程序中使用Jetty而不是Tomcat? +### 6. 如何在 Spring Boot 应用程序中使用 Jetty 而不是 Tomcat? -Spring Boot (`spring-boot-starter-web`)使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改`pom.xml`(Maven)或者`build.gradle`(Gradle)就可以了。 +Spring Boot (`spring-boot-starter-web`)使用 Tomcat 作为默认的嵌入式 servlet 容器, 如果你想使用 Jetty 的话只需要修改`pom.xml`(Maven)或者`build.gradle`(Gradle)就可以了。 **Maven:** @@ -92,7 +94,7 @@ compile("org.springframework.boot:spring-boot-starter-jetty") 说个题外话,从上面可以看出使用 Gradle 更加简洁明了,但是国内目前还是 Maven 使用的多一点,我个人觉得 Gradle 在很多方面都要好很多。 -### 介绍一下@SpringBootApplication注解 +### 7. 介绍一下@SpringBootApplication 注解 ```java package org.springframework.boot.autoconfigure; @@ -121,19 +123,19 @@ public @interface SpringBootConfiguration { } ``` -可以看出大概可以把 `@SpringBootApplication `看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan ` 注解的集合。根据 SpringBoot官网,这三个注解的作用分别是: +可以看出大概可以把 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是: - `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 -- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的bean,注解默认会扫描该类所在的包下所有的类。 -- `@Configuration`:允许在上下文中注册额外的bean或导入其他配置类 +- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 +- `@Configuration`:允许在上下文中注册额外的 bean 或导入其他配置类 -### Spring Boot 的自动配置是如何实现的? +### 8. Spring Boot 的自动配置是如何实现的? -这个是因为`@SpringBootApplication `注解的原因,在上一个问题中已经提到了这个注解。我们知道 `@SpringBootApplication `看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan ` 注解的集合。 +这个是因为`@SpringBootApplication`注解的原因,在上一个问题中已经提到了这个注解。我们知道 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。 - `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 -- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的bean,注解默认会扫描该类所在的包下所有的类。 -- `@Configuration`:允许在上下文中注册额外的bean或导入其他配置类 +- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 +- `@Configuration`:允许在上下文中注册额外的 bean 或导入其他配置类 `@EnableAutoConfiguration`是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程): @@ -161,9 +163,9 @@ public @interface EnableAutoConfiguration { } ``` -`@EnableAutoConfiguration` 注解通过Spring 提供的 `@Import` 注解导入了`AutoConfigurationImportSelector`类(`@Import` 注解可以导入配置类或者Bean到当前类中)。 +`@EnableAutoConfiguration` 注解通过 Spring 提供的 `@Import` 注解导入了`AutoConfigurationImportSelector`类(`@Import` 注解可以导入配置类或者 Bean 到当前类中)。 -` ``AutoConfigurationImportSelector`类中`getCandidateConfigurations`方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。 +`AutoConfigurationImportSelector`类中`getCandidateConfigurations`方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。 ```java protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { @@ -177,9 +179,9 @@ public @interface EnableAutoConfiguration { 自动配置信息有了,那么自动配置还差什么呢? -`@Conditional` 注解。`@ConditionalOnClass`(指定的类必须存在于类路径下),`@ConditionalOnBean`(容器中是否有指定的Bean)等等都是对`@Conditional`注解的扩展。拿 Spring Security 的自动配置举个例子: +`@Conditional` 注解。`@ConditionalOnClass`(指定的类必须存在于类路径下),`@ConditionalOnBean`(容器中是否有指定的 Bean)等等都是对`@Conditional`注解的扩展。 -`SecurityAutoConfiguration`中导入了`WebSecurityEnablerConfiguration`类,`WebSecurityEnablerConfiguration`源代码如下: +拿 Spring Security 的自动配置举个例子:`SecurityAutoConfiguration`中导入了`WebSecurityEnablerConfiguration`类,`WebSecurityEnablerConfiguration`源代码如下: ```java @Configuration @@ -194,9 +196,9 @@ public class WebSecurityEnablerConfiguration { `WebSecurityEnablerConfiguration`类中使用`@ConditionalOnBean`指定了容器中必须还有`WebSecurityConfigurerAdapter` 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 `WebSecurityConfigurerAdapter`,这样自动将配置就完成了。 -### 开发 RESTful Web 服务常用的注解有哪些? +### 9. 开发 RESTful Web 服务常用的注解有哪些? -**Spring Bean相关:** +**Spring Bean 相关:** - `@Autowired` : 自动导入对象到类中,被注入进的类同样要被 Spring 容器管理。 - `@RestController` : `@RestController`注解是`@Controller和`@`ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。 @@ -207,23 +209,23 @@ public class WebSecurityEnablerConfiguration { **处理常见的 HTTP 请求类型:** -- `@GetMapping` : GET请求、 -- `@PostMapping` : POST请求。 -- `@PutMapping` : PUT请求。 -- `@DeleteMapping` : DELETE请求。 +- `@GetMapping` : GET 请求、 +- `@PostMapping` : POST 请求。 +- `@PutMapping` : PUT 请求。 +- `@DeleteMapping` : DELETE 请求。 **前后端传值:** - `@RequestParam`以及`@Pathvairable :@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。` - `@RequestBody` :用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且 Content-Type 为 `application/json` 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 -详细介绍可以查看这篇文章:[《Spring/Spring Boot常用注解总结》](https://snailclimb.gitee.io/javaguide/#/./docs/system-design/framework/spring/SpringBoot+Spring%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3%E6%80%BB%E7%BB%93?id=_21-autowired) 。 +详细介绍可以查看这篇文章:[《Spring/Spring Boot 常用注解总结》](https://snailclimb.gitee.io/javaguide/#/./docs/system-design/framework/spring/SpringBoot+Spring%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3%E6%80%BB%E7%BB%93?id=_21-autowired) 。 -### Spirng Boot 常用的两种配置文件 +### 10. Spirng Boot 常用的两种配置文件 我们可以通过 `application.properties`或者 `application.yml` 对 Spring Boot 程序进行简单的配置。如果,你不进行配置的话,就是使用的默认配置。 -### 什么是 YAML?YAML 配置的优势在哪里 ? +### 11. 什么是 YAML?YAML 配置的优势在哪里 ? YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。 @@ -233,7 +235,7 @@ YAML 是一种人类可读的数据序列化语言。它通常用于配置文件 但是,YAML 配置的方式有一个缺点,那就是不支持 `@PropertySource` 注解导入自定义的 YAML 配置。 -### Spring Boot 常用的读取配置文件的方法有哪些? +### 12. Spring Boot 常用的读取配置文件的方法有哪些? 我们要读取的配置文件`application.yml` 内容如下: @@ -255,9 +257,9 @@ library: description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? ``` -#### 通过 `@value` 读取比较简单的配置信息 +#### 12.1. 通过 `@value` 读取比较简单的配置信息 -使用 `@Value("${property}")` 读取比较简单的配置信息: +使用 `@Value("${property}")` 读取比较简单的配置信息: ```java @Value("${wuhan2020}") @@ -266,9 +268,9 @@ String wuhan2020; > **需要注意的是 `@value`这种方式是不被推荐的,Spring 比较建议的是下面几种读取配置信息的方式。** -#### 通过`@ConfigurationProperties`读取并与 bean 绑定 +#### 12.2. 通过`@ConfigurationProperties`读取并与 bean 绑定 -> **`LibraryProperties` 类上加了 `@Component` 注解,我们可以像使用普通 bean 一样将其注入到类中使用。** +> **`LibraryProperties` 类上加了 `@Component` 注解,我们可以像使用普通 bean 一样将其注入到类中使用。** ```java @@ -340,7 +342,7 @@ public class ReadConfigPropertiesApplication implements InitializingBean { [LibraryProperties.Book(name=天才基本法, description........] ``` -#### 通过`@ConfigurationProperties`读取并校验 +#### 12.3. 通过`@ConfigurationProperties`读取并校验 我们先将`application.yml`修改为如下内容,明显看出这不是一个正确的 email 格式: @@ -350,9 +352,9 @@ my-profile: email: koushuangbwcx@ ``` ->**`ProfileProperties` 类没有加 `@Component` 注解。我们在我们要使用`ProfileProperties` 的地方使用`@EnableConfigurationProperties`注册我们的配置bean:** +> **`ProfileProperties` 类没有加 `@Component` 注解。我们在我们要使用`ProfileProperties` 的地方使用`@EnableConfigurationProperties`注册我们的配置 bean:** - ```java +```java import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -364,26 +366,26 @@ import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; /** - * @author shuang.kou - */ +* @author shuang.kou +*/ @Getter @Setter @ToString @ConfigurationProperties("my-profile") @Validated public class ProfileProperties { - @NotEmpty - private String name; + @NotEmpty + private String name; - @Email - @NotEmpty - private String email; - - //配置文件中没有读取到的话就用默认值 - private Boolean handsome = Boolean.TRUE; + @Email + @NotEmpty + private String email; + + //配置文件中没有读取到的话就用默认值 + private Boolean handsome = Boolean.TRUE; } - ``` +``` 具体使用: @@ -437,7 +439,7 @@ Binding to target org.springframework.boot.context.properties.bind.BindException ProfileProperties(name=Guide哥, email=koushuangbwcx@163.com, handsome=true) ``` -#### `@PropertySource`读取指定的 properties 文件 +#### 12.4. `@PropertySource`读取指定的 properties 文件 ```java import lombok.Getter; @@ -466,7 +468,7 @@ System.out.println(webSite.getUrl());//https://javaguide.cn/ ``` -### Spring Boot加载配置文件的优先级了解么? +### 13. Spring Boot 加载配置文件的优先级了解么? Spring 读取配置文件也是有优先级的,直接上图: @@ -474,42 +476,57 @@ Spring 读取配置文件也是有优先级的,直接上图: 更对内容请查看官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config -### 常用的 Bean 映射工具有哪些? +### 14. 常用的 Bean 映射工具有哪些? - MapStruct、ModelMapper、Dozer、Orika、JMapper是 5 种常用的 Bean 映射工具。 +MapStruct、ModelMapper、Dozer、Orika、JMapper 是 5 种常用的 Bean 映射工具。 综合日常使用情况和相关测试数据,个人感觉 MapStruct、ModelMapper 这两个 Bean 映射框架是最佳选择。 -### Spring Boot 如何做请求参数校验? +### 15. Spring Boot 如何监控系统实际运行状况? + +我们可以使用 Spring Boot Actuator 来对 Spring Boot 项目进行简单的监控。 + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + +集成了这个模块之后,你的 Spring Boot 应用程序就自带了一些开箱即用的获取程序运行时的内部状态信息的 API。 + +比如通过 GET 方法访问 `/health` 接口,你就可以获取应用程序的健康指标。 + +### 16. Spring Boot 如何做请求参数校验? 数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。 Spring Boot 程序做请求参数校验的话只需要`spring-boot-starter-web` 依赖就够了,它的子依赖包含了我们所需要的东西。 -#### 校验注解 +#### 16.1. 校验注解 -**JSR提供的校验注解**: +**JSR 提供的校验注解**: - `@Null` 被注释的元素必须为 null - `@NotNull` 被注释的元素必须不为 null - `@AssertTrue` 被注释的元素必须为 true - `@AssertFalse` 被注释的元素必须为 false -- `@Min(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@Max(value) ` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@DecimalMin(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 +- `@Min(value)` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 +- `@Max(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 +- `@DecimalMin(value)` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 - `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@Size(max=, min=) ` 被注释的元素的大小必须在指定的范围内 -- `@Digits (integer, fraction) ` 被注释的元素必须是一个数字,其值必须在可接受的范围内 -- `@Past ` 被注释的元素必须是一个过去的日期 +- `@Size(max=, min=)` 被注释的元素的大小必须在指定的范围内 +- `@Digits (integer, fraction)` 被注释的元素必须是一个数字,其值必须在可接受的范围内 +- `@Past` 被注释的元素必须是一个过去的日期 - `@Future` 被注释的元素必须是一个将来的日期 -- `@Pattern(regex=,flag=) ` 被注释的元素必须符合指定的正则表达式 +- `@Pattern(regex=,flag=)` 被注释的元素必须符合指定的正则表达式 -**Hibernate Validator提供的校验注解**: +**Hibernate Validator 提供的校验注解**: -- `@NotBlank(message =) ` 验证字符串非null,且长度必须大于0 +- `@NotBlank(message =)` 验证字符串非 null,且长度必须大于 0 - `@Email` 被注释的元素必须是电子邮箱地址 -- `@Length(min=,max=) ` 被注释的字符串的大小必须在指定的范围内 -- `@NotEmpty ` 被注释的字符串的必须非空 +- `@Length(min=,max=)` 被注释的字符串的大小必须在指定的范围内 +- `@NotEmpty` 被注释的字符串的必须非空 - `@Range(min=,max=,message=)` 被注释的元素必须在合适的范围内 **使用示例:** @@ -538,9 +555,9 @@ public class Person { } ``` -#### 验证请求体(RequestBody) +#### 16.2. 验证请求体(RequestBody) -我们在需要验证的参数上加上了`@Valid` 注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。 +我们在需要验证的参数上加上了`@Valid` 注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。 ```java @RestController @@ -554,7 +571,7 @@ public class PersonController { } ``` -#### 验证请求参数(Path Variables 和 Request Parameters) +#### 16.3. 验证请求参数(Path Variables 和 Request Parameters) 一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。 @@ -578,13 +595,13 @@ public class PersonController { 更多内容请参考我的原创: **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://snailclimb.gitee.io/springboot-guide/#/./docs/advanced/spring-bean-validation)** -### 如何使用 Spring Boot 实现全局异常处理? +### 17. 如何使用 Spring Boot 实现全局异常处理? 可以使用 `@ControllerAdvice` 和 `@ExceptionHandler` 处理全局异常。 更多内容请参考我的原创 :[Spring Boot 异常处理在实际项目中的应用](https://snailclimb.gitee.io/springboot-guide/#/./docs/advanced/springboot-handle-exception-plus) -### Spring Boot 中如何实现定时任务 ? +### 18. Spring Boot 中如何实现定时任务 ? 我们使用 `@Scheduled` 注解就能很方便地创建一个定时任务。 @@ -605,7 +622,4 @@ public class ScheduledTasks { } ``` -单纯依靠 `@Scheduled` 注解 还不行,我们还需要在 SpringBoot 中我们只需要在启动类上加上`@EnableScheduling` 注解,这样才可以启动定时任务。`@EnableScheduling` 注解的作用是发现注解 `@Scheduled` 的任务并在后台执行该任务。 - - - +单纯依靠 `@Scheduled` 注解 还不行,我们还需要在 SpringBoot 中我们只需要在启动类上加上`@EnableScheduling` 注解,这样才可以启动定时任务。`@EnableScheduling` 注解的作用是发现注解 `@Scheduled` 的任务并在后台执行该任务。 \ No newline at end of file From 2fc103e0dfaee041eecf8fe472990e039fd88131 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 31 Dec 2020 15:56:43 +0800 Subject: [PATCH 181/207] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-generator-freemarker/.gitignore | 33 -- .../.mvn/wrapper/MavenWrapperDownloader.java | 118 ------- .../.mvn/wrapper/maven-wrapper.jar | Bin 50710 -> 0 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 - .../practice/code-generator-freemarker/mvnw | 322 ------------------ .../code-generator-freemarker/mvnw.cmd | 182 ---------- .../code-generator-freemarker/pom.xml | 65 ---- .../CodeGeneratorFreemarkerApplication.java | 13 - .../javaguide/rest/common/ResponseResult.java | 58 ---- .../cn/javaguide/rest/common/ResultCode.java | 21 -- .../controller/CodeGeneratorController.java | 15 - .../service/CodeGeneratorService.java | 11 - .../main/java/cn/javaguide/util/DbUtil.java | 27 -- .../src/main/resources/application.properties | 1 - ...deGeneratorFreemarkerApplicationTests.java | 13 - 15 files changed, 881 deletions(-) delete mode 100644 source-code/practice/code-generator-freemarker/.gitignore delete mode 100644 source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java delete mode 100644 source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.jar delete mode 100644 source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties delete mode 100755 source-code/practice/code-generator-freemarker/mvnw delete mode 100644 source-code/practice/code-generator-freemarker/mvnw.cmd delete mode 100644 source-code/practice/code-generator-freemarker/pom.xml delete mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java delete mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java delete mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java delete mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java delete mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java delete mode 100644 source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java delete mode 100644 source-code/practice/code-generator-freemarker/src/main/resources/application.properties delete mode 100644 source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java diff --git a/source-code/practice/code-generator-freemarker/.gitignore b/source-code/practice/code-generator-freemarker/.gitignore deleted file mode 100644 index 549e00a..0000000 --- a/source-code/practice/code-generator-freemarker/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -HELP.md -target/ -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### 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/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java b/source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index a45eb6b..0000000 --- a/source-code/practice/code-generator-freemarker/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if (mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if (mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if (!outputFile.getParentFile().exists()) { - if (!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.jar b/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50710 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 EfAhh=DF6Tf diff --git a/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties b/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572..0000000 --- a/source-code/practice/code-generator-freemarker/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/source-code/practice/code-generator-freemarker/mvnw b/source-code/practice/code-generator-freemarker/mvnw deleted file mode 100755 index 3c8a553..0000000 --- a/source-code/practice/code-generator-freemarker/mvnw +++ /dev/null @@ -1,322 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ]; then - - if [ -f /etc/mavenrc ]; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ]; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false -darwin=false -mingw=false -case "$(uname)" in -CYGWIN*) cygwin=true ;; -MINGW*) mingw=true ;; -Darwin*) - darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="$(/usr/libexec/java_home)" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ]; then - if [ -r /etc/gentoo-release ]; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -if [ -z "$M2_HOME" ]; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ]; do - ls=$(ls -ld "$PRG") - link=$(expr "$ls" : '.*-> \(.*\)$') - if expr "$link" : '/.*' >/dev/null; then - PRG="$link" - else - PRG="$(dirname "$PRG")/$link" - fi - done - - saveddir=$(pwd) - - M2_HOME=$(dirname "$PRG")/.. - - # make it fully qualified - M2_HOME=$(cd "$M2_HOME" && pwd) - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=$(cygpath --unix "$M2_HOME") - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --unix "$CLASSPATH") -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw; then - [ -n "$M2_HOME" ] && - M2_HOME="$( ( - cd "$M2_HOME" - pwd - ))" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="$( ( - cd "$JAVA_HOME" - pwd - ))" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr \"$javaExecutable\" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! $(expr "$readLink" : '\([^ ]*\)') = "no" ]; then - if $darwin; then - javaHome="$(dirname \"$javaExecutable\")" - javaExecutable="$(cd \"$javaHome\" && pwd -P)/javac" - else - javaExecutable="$(readlink -f \"$javaExecutable\")" - fi - javaHome="$(dirname \"$javaExecutable\")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ]; then - if [ -n "$JAVA_HOME" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="$(which java)" - fi -fi - -if [ ! -x "$JAVACMD" ]; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ]; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ]; then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ]; do - if [ -d "$wdir"/.mvn ]; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$( - cd "$wdir/.." - pwd - ) - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' <"$1")" - fi -} - -BASE_DIR=$(find_maven_basedir "$(pwd)") -if [ -z "$BASE_DIR" ]; then - exit 1 -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in wrapperUrl) - jarUrl="$value" - break - ;; - esac - done <"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi - - if command -v wget >/dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl >/dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=$(cygpath --path --windows "$M2_HOME") - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/source-code/practice/code-generator-freemarker/mvnw.cmd b/source-code/practice/code-generator-freemarker/mvnw.cmd deleted file mode 100644 index c8d4337..0000000 --- a/source-code/practice/code-generator-freemarker/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/source-code/practice/code-generator-freemarker/pom.xml b/source-code/practice/code-generator-freemarker/pom.xml deleted file mode 100644 index f0e5a0a..0000000 --- a/source-code/practice/code-generator-freemarker/pom.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.4.1 - - - cn.javaguide - code-generator-freemarker - 0.0.1-SNAPSHOT - code-generator-freemarker - Demo project for Spring Boot - - - 8 - - 1.7.25 - - - - - - org.springframework.boot - spring-boot-starter-freemarker - - - org.springframework.boot - spring-boot-starter-web - - - mysql - mysql-connector-java - runtime - - - org.projectlombok - lombok - 1.18.16 - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java deleted file mode 100644 index 17381f4..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/CodeGeneratorFreemarkerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package cn.javaguide; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class CodeGeneratorFreemarkerApplication { - - public static void main(String[] args) { - SpringApplication.run(CodeGeneratorFreemarkerApplication.class, args); - } - -} \ No newline at end of file diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java deleted file mode 100644 index 3dd1ad6..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResponseResult.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.javaguide.rest.common; - - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class ResponseResult { - /** - * 状态码 - */ - private Integer status; - /** - * 提示信息 - */ - private String msg; - /** - * 数据封装 - */ - private T obj; - - protected ResponseResult(Integer status, String msg, T obj) { - this.status = status; - this.msg = msg; - this.obj = obj; - } - - public static ResponseResult success(T data) { - return new ResponseResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); - } - - public static ResponseResult success(String message) { - return new ResponseResult(ResultCode.SUCCESS.getCode(), message, null); - } - - public static ResponseResult success(String message, T data) { - return new ResponseResult(ResultCode.SUCCESS.getCode(), message, data); - } - - - public static ResponseResult failed(ResultCode errorCode) { - return new ResponseResult(errorCode.getCode(), errorCode.getMessage(), null); - } - - public static ResponseResult failed(ResultCode errorCode, String message) { - return new ResponseResult(errorCode.getCode(), message, null); - } - - public static ResponseResult failed(String message) { - return new ResponseResult(ResultCode.FAILED.getCode(), message, null); - } - - public static ResponseResult failed() { - return failed(ResultCode.FAILED); - } - -} \ No newline at end of file diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java deleted file mode 100644 index 1037ae0..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/common/ResultCode.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.javaguide.rest.common; - -public enum ResultCode { - SUCCESS(200, "操作成功"), - FAILED(500, "操作失败"); - private Integer code; - private String message; - - ResultCode(Integer code, String message) { - this.code = code; - this.message = message; - } - - public Integer getCode() { - return code; - } - - public String getMessage() { - return message; - } -} \ No newline at end of file diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java deleted file mode 100644 index afe376c..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/rest/controller/CodeGeneratorController.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.javaguide.rest.controller; - -import cn.javaguide.service.CodeGeneratorService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author shuang.kou - * @createTime 2020年12月31日 14:03:00 - **/ -@RestController -@RequiredArgsConstructor -public class CodeGeneratorController { - private final CodeGeneratorService codeGeneratorService; -} diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java deleted file mode 100644 index 983ec64..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/service/CodeGeneratorService.java +++ /dev/null @@ -1,11 +0,0 @@ -package cn.javaguide.service; - -import org.springframework.stereotype.Service; - -/** - * @author shuang.kou - * @createTime 2020年12月31日 14:03:00 - **/ -@Service -public class CodeGeneratorService { -} diff --git a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java b/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java deleted file mode 100644 index 221eb38..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/java/cn/javaguide/util/DbUtil.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.javaguide.util; - -import lombok.extern.slf4j.Slf4j; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; - -@Slf4j -public class DbUtil { - public static final String MYSQL_JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; - private static Connection connection; - - public static Connection getConnection() { - return connection; - } - - public static Connection getConnection(String url, String username, String password) throws ClassNotFoundException, SQLException { - if (connection == null) { - Class.forName(MYSQL_JDBC_DRIVER); - log.info("MySQL 数据库驱动加载成功"); - connection = DriverManager.getConnection(url, username, password); - } - return connection; - } - -} diff --git a/source-code/practice/code-generator-freemarker/src/main/resources/application.properties b/source-code/practice/code-generator-freemarker/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/source-code/practice/code-generator-freemarker/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java b/source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java deleted file mode 100644 index ef2f168..0000000 --- a/source-code/practice/code-generator-freemarker/src/test/java/cn/javaguide/CodeGeneratorFreemarkerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package cn.javaguide; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class CodeGeneratorFreemarkerApplicationTests { - - @Test - void contextLoads() { - } - -} From 52fb0393a2e474daec2171087413477796280cd7 Mon Sep 17 00:00:00 2001 From: guide Date: Fri, 1 Jan 2021 00:50:06 +0800 Subject: [PATCH 182/207] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=B8=A4?= =?UTF-8?q?=E4=B8=AA=E5=AE=9E=E6=88=98=E9=A1=B9=E7=9B=AE=EF=BC=9A=E5=88=B7?= =?UTF-8?q?=E9=A2=98=E7=B3=BB=E7=BB=9F+=E8=80=83=E8=AF=95=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- ...67\351\242\230\347\263\273\347\273\237.md" | 121 ++++++++++++ ...03\350\257\225\347\263\273\347\273\237.md" | 174 ++++++++++++++++++ 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 "docs/projects/SpringCloud\345\210\267\351\242\230\347\263\273\347\273\237.md" create mode 100644 "docs/projects/\344\270\200\344\270\252\345\237\272\344\272\216SpringBoot\347\232\204\345\234\250\347\272\277\350\200\203\350\257\225\347\263\273\347\273\237.md" diff --git a/README.md b/README.md index 0c73bb8..f4a2c81 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,10 @@ ## 实战项目 -1. [Spring Boot搭建的一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-基于Spring Boot在线文件预览系统.md) -2. [一份热乎的 SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC权限控制...](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247495011&idx=1&sn=f574f5d75c3720d8b2a665d1d5234d28&chksm=cea1a2a8f9d62bbe9f13f5a030893fe3da6956c4be41471513e6247f74cba5a8df9941798b6e&token=212861022&lang=zh_CN#rd) +1. [使用 Spring Boot搭建一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-基于Spring Boot在线文件预览系统.md) +2. [ SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC权限控制...](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247495011&idx=1&sn=f574f5d75c3720d8b2a665d1d5234d28&chksm=cea1a2a8f9d62bbe9f13f5a030893fe3da6956c4be41471513e6247f74cba5a8df9941798b6e&token=212861022&lang=zh_CN#rd) +3. [一个基于Spring Cloud 的面试刷题系统。](./docs/projects/SpringCloud刷题系统.md) +4. [一个基于 Spring Boot 的在线考试系统](./docs/projects/一个基于SpringBoot的在线考试系统.md) ## 面试题 diff --git "a/docs/projects/SpringCloud\345\210\267\351\242\230\347\263\273\347\273\237.md" "b/docs/projects/SpringCloud\345\210\267\351\242\230\347\263\273\347\273\237.md" new file mode 100644 index 0000000..2309eab --- /dev/null +++ "b/docs/projects/SpringCloud\345\210\267\351\242\230\347\263\273\347\273\237.md" @@ -0,0 +1,121 @@ +> 推荐👍: +> - [接近100K star 的 Java 学习/面试指南](https://github.com/Snailclimb/JavaGuide) +> - [Github 95k+点赞的 Java 面试/学习手册.PDF](https://www.yuque.com/docs/share/f0c5ffbc-48b5-45f3-af66-2a5e6f212595) + +今天给小伙伴们推荐一个朋友开源的面试刷题系统。 + +这篇文章我会从系统架构设计层面详解介绍这个开源项目,并且会把微服务常用的一些技术都介绍一下。即使你对这个项目不感兴趣,也能了解到很多微服务相关的知识。美滋滋! + +_昨晚肝了很久~原创不易,若有帮助,求赞求转发啊!_ + +不得不说,这个刷题系统确实是有点东西,你真的值得拥有!首先,这是一个微服务的项目,其次这个系统涵盖了市面上常用的主流技术比如 SpringBoot、Spring Cloud 等等(后面会详细介绍)。 + +不论是你想要学习分布式的技术,还是想找一个实战项目练手或者作为自己的项目经验,这个项目都非常适合你。 + +另外,因为项目作者提供了详细的技术文档,所以你不用担心上手太难! + +![PassJava文档](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12-21/PassJava%E6%96%87%E6%A1%A3%E5%9C%B0%E5%9D%80.png) + +## 效果图 + +我们先来看看这个面试刷题系统的效果图。这里我们只展示的是这个系统的前端(微信小程序),后台管理系统这里就不展示了。 + +![PassJava前端展示](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12-21/PassJava%E5%89%8D%E7%AB%AF%E5%B1%95%E7%A4%BA.png) + +可以看到,除了少部分地方的颜色搭配比较难看之外,页面整体 UI 还是比较美观的。 + +## 技术栈 + +再聊聊大家最关心的问题:“**这套系统的技术栈是什么样的呢?**”。 + +这套系统采用了目前企业都在用的主流技术:SpringBoot(基础框架)、Spring Cloud(微服务)、MyBatis(ORM框架)、Redis(缓存)、MySql(关系型数据库)、MongoDB(NoSQL)、RabbitMQ(消息队列)、Elasticsearch(搜索引擎)。并且,这个系统是以 Docker 容器化的方式进行部署的。非常实用! + +## 系统架构设计 + +了解了技术栈之后,那必然需要简单了解一下整个 **系统的架构设计** ,这是系统的灵魂所在了(图源:[PassJava 官方文档](http://jayh2018.gitee.io/passjava-learning/#/01.项目简介/2.项目微服务架构图 "PassJava官方文档"))。 + +![](http://cdn.jayh.club/blog/20200407/scg1XhlvGbUV.png?imageslim) + +### 网关 + +网关负责认证授权、限流、熔断、降级、请求分发、负载均衡等等操作。一般情况下,网关一般都会提供这些功能。 + +这里使用的是 **Spring Cloud Gateway** 作为网关。Spring Cloud Gateway 是 Spring Cloud 官方推出的第二代网关框架,目的是取代 netflix 的 Zuul 网关。 + +### 注册中心和配置中心 + +注册中心和配置中心这块使用的是阿里巴巴开源的 **Nacos** 。Nacos 目前属于 Spring Cloud Alibaba 中的一员。主要用于发现、配置和管理微服务,类似于 Consul、Eureka。并且,提供了分布式配置管理功能。 + +Nacos 的基本介绍如下(图源:[官网文档-什么是 Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html "官网文档-什么是 Nacos")): + +![nacos_map](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12-21/nacosMap.jpg) + +详解介绍一下 Nacos 在这个项目中提供的两个核心功能: + +- **注册中心** :API 网关通过注册中心实时获取到服务的路由地址,准确地将请求路由到各个服务。 +- **配置中心** :传统的配置方式需要重新启动服务。如果服务很多,则需要重启所有服务,非常不方便。通过 Nacos,我们可以动态配置服务。并且,Nacos 提供了一个简洁易用的 UI 帮助我们管理所有的服务和应用的配置。 + +关于配置中心,我们这里再拓展一下,除了 Nacos ,还有 Apollo、SpringCloud Config、K8s ConfigMap 可供选择。 + +### 分布式链路追踪 + +> 不同于单体架构,在分布式架构下,请求需要在多个服务之间调用,排查问题会非常麻烦。我们需要分布式链路追踪系统来解决这个痛点。 + +分布式链路追踪这块使用的是 Twitter 的 **Zipkin** ,并且结合了 **Spring Cloud Sleuth** 。 + +Spring Cloud Sleuth 只是做一些链路追踪相关的数据记录,我们可以使用 Zipkin Server 来处理这些数据。 + +相关阅读:[《40 张图看懂分布式追踪系统原理及实践》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247492112&idx=3&sn=4eb100f4ecef98a475d7f7650c993222&chksm=cea1addbf9d624cdc0e868d3b72edf40a7a6880218ecb1a9ddbac50423d20dceceb8dca804f4&token=2007747701&lang=zh_CN#rd) 。 + +### 监控系统 + +监控系统可以帮助我们监控应用程序的状态,并且能够在风险发生前告警。 + +监控系统这块使用的是 **Prometheus + Grafana**。Prometheus 负责收集监控数据,Grafana 用于展示监控数据。我们直接将 Grafana 的数据源选择为 Prometheus 即可。 + +关于监控系统更详细的技术选型,可以看这篇文章:[《监控系统选型看这一篇够了!选择 Prometheus 还是 Zabbix ?》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247490068&idx=2&sn=3ac42f6891fec8906230cc9e0ec28ba7&chksm=cea255dff9d5dcc9369c9c30eb55925cd16045d914cbfe194ea7d5630c3339630d731d654527&token=2007747701&lang=zh_CN#rd) 。 + +### 消息队列 + +我们知道,消息队列主要能为系统带来三点好处: + +1. **通过异步处理提高系统性能(减少响应所需时间)。** +2. **削峰/限流** +3. **降低系统耦合性。** + +常用的消息队列有:RabbitMQ(本系统所采用的方案)、Kafka、RocketMQ。 + +### 缓存 + +缓存这里使用的是 Redis ,老生常谈了,这里就不再多做介绍。 + +另外, 为了保证缓存服务的高可用,我们使用 Redis 官方提供了一种 Redis 集群的解决方案 Redis Sentinel 来管理 Redis 集群。 + +### 数据库 + +数据库这里使用的是 MySQL ,并使用主从模式实现读写分离,以提高读性能。 + +### 对象存储 + +由于是分布式系统,传统的将文件上传到本机已经没办法满足我们的需求了。 + +由于自己搭建分布式文件系统也比较麻烦,所以对象存储这里我们使用的是阿里云 OSS,它主要用于存储一些文件比如图片。 + +### 快速开发脚手架 + +另外,为了后台的快速搭建这里使用的是 **[renren-fast](https://gitee.com/renrenio/renren-fast "renren-fast")** 快速开发脚手架。使用这个脚手架配合上代码生成器 **[renren-generator](https://gitee.com/renrenio/renren-generator "renren-generator")** ,我们可以快速生成 70%左右的前后端代码。绝对是快速开发项目并交付以及接私活的利器了! + +我在之前的也推荐过这个脚手架,详情请看下面这两篇文章: + +1. [听说你要接私活?Guide 连夜整理了 5 个开源免费的 Java 项目快速开发脚手架。](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247487002&idx=1&sn=e9db79f1bbd561b3ead1c9475f8fd7f5&chksm=cea241d1f9d5c8c72fa33764208a26a26de5d2d3e4ac2a6794e0792ba779bb11414b3fd506e3&token=2007747701&lang=zh_CN#rd) +2. [解放双手,再来推荐 5 个 Java 项目开发快速开发脚手架!项目经验和私活都不愁了!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247487467&idx=1&sn=c2d2bd28918b58d8646f1441791aeaaf&chksm=cea24020f9d5c9364afc22013fac3266fabffcaa370f708434a6a304274c6b1d8bce235b7a24&token=2007747701&lang=zh_CN#rd) + +## 总结 + +这篇文章我主要从架构设计层面分析了朋友开源的这个基于微服务的刷题系统。 + +当然了,朋友使用微服务开发这个项目的主要目的也是为了自己实践微服务相关的知识,同时也是为了给需要微服务相关实战项目经验的小伙伴一个可以学习的项目。不然的话,直接用单体就完事了,完全可以支撑这个项目目前的并发量以及可预见的未来的并发量。 + +- 项目地址:https://github.com/Jackson0714/PassJava-Platform +- 文档地址:http://jayh2018.gitee.io/passjava-learning/#/ + diff --git "a/docs/projects/\344\270\200\344\270\252\345\237\272\344\272\216SpringBoot\347\232\204\345\234\250\347\272\277\350\200\203\350\257\225\347\263\273\347\273\237.md" "b/docs/projects/\344\270\200\344\270\252\345\237\272\344\272\216SpringBoot\347\232\204\345\234\250\347\272\277\350\200\203\350\257\225\347\263\273\347\273\237.md" new file mode 100644 index 0000000..0c92bf0 --- /dev/null +++ "b/docs/projects/\344\270\200\344\270\252\345\237\272\344\272\216SpringBoot\347\232\204\345\234\250\347\272\277\350\200\203\350\257\225\347\263\273\347\273\237.md" @@ -0,0 +1,174 @@ +[大家好!我是 Guide 哥,Java 后端开发。一个会一点前端,喜欢烹饪的自由少年。](https://www.yuque.com/docs/share/71251673-1fef-416e-93d7-489a25a9eda5?#%20%E3%80%8A%E8%B5%B0%E8%BF%91JavaGuide%E3%80%8B) + +## 前言 + +最近看到了一个考试系统,感觉做的挺不错,并且也比较成熟,所以我就简单玩了一下。另外,考试系统应用场景还挺多的,不论是对于在校大学生还是已经工作的小伙伴,并且,类似的私活也有很多。 + +![在线考试系统后台管理主页](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/FireShot%20Capture%20027%20-%20%E4%B8%BB%E9%A1%B5%20-%20localhost.png) + +下面我就把这个项目分享给小伙伴们,非常值得学习,拿来即用! + +_为了一步一步演示,让小伙伴们都能成功部署/运行项目, Guide 哥自己本地搭建了项目环境,并将项目成功跑了起来。照着我的步骤,新手也能成功把项目跑起来!_ + +_如果你“感动”的话,点个赞/在看,就是对我最大的支持!_ + +另外,以下内容不涉及代码分析,整体代码结构比较清晰,熟悉了基本功能之后会很容易看明白。 + +## 介绍 + +[uexam](https://gitee.com/mindskip/uexam) 是一款前后端分离的在线考试系统。这款在线考试系统,不光支持 web 端,同时还支持微信小程序端。 + +[uexam](https://gitee.com/mindskip/uexam) 界面设计美观,代码整体结构清晰,表设计比较规范。 + +[uexam](https://gitee.com/mindskip/uexam) 后端基于 Spring Boot 2.0+MySQL/PostgreSQL+Redis+MyBatis,前端基于 Vue,采用前端后端分离开发! + +另外,这个项目提供了 MySQL 和 PostgreSQL 两种不同的数据库版本,下面我以 PostgreSQL 数据库版本的来演示(_建议大家使用和体验 PostgreSQL 版本_)。 + +项目地址:[https://gitee.com/SnailClimb/uexam](https://gitee.com/SnailClimb/uexam) 。 + +## 软件架构 + +![软件架构图](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84%E5%9B%BE.jpg) + +## 使用效果 + +样式以及操作体验都是非常不错的,这也是我推荐这个项目很重要的一个原因。 + +### 管理端 + +#### 添加学科 + +在创建题目之前,你需要首要创建学科。这里我们创建的学科是编程,年级是三年级。 + +![添加学科](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E6%B7%BB%E5%8A%A0%E5%AD%A6%E7%A7%91.jpg) + +#### 添加题目 + +可以看到这里可以添加多种题型: 单选题、多选题、判断题、填空题、简答题。 + +![添加题目](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E6%B7%BB%E5%8A%A0%E9%A2%98%E7%9B%AE.jpg) + +我们以单选题为例,添加题目界面如下。 + +![添加题目页面](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/iShot2020-09-03%E4%B8%8B%E5%8D%8805.25.30.jpg) + +添加成功之后,题目列表就会出现我们刚刚添加的题目。 + +![题目创建成功](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E9%A2%98%E7%9B%AE%E5%88%9B%E5%BB%BA%E6%88%90%E5%8A%9F.jpg) + +#### 添加试卷 + +有了学科和题目之后才能添加试卷。 + +![添加试卷](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E6%B7%BB%E5%8A%A0%E8%AF%95%E5%8D%B7.jpg) + +添加成功之后,试卷列表就会出现我们刚刚添加的试卷。 + +![试卷创建成功](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E8%AF%95%E5%8D%B7%E5%88%9B%E5%BB%BA%E6%88%90%E5%8A%9F.jpg) + +#### 添加学生 + +**注意:这里的学生要和我们前面创建的学科对应的年级对应上。** + +![添加学生](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E6%B7%BB%E5%8A%A0%E5%AD%A6%E7%94%9F.jpg) + +### 学生端 + +使用我们刚刚创建的学生账号登录,你会发现主页多了一个试卷。这个试卷就是我们刚刚在管理端创建的。 + +![学生端-主页](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E5%AD%A6%E7%94%9F%E7%AB%AF-%E4%B8%BB%E9%A1%B5.jpg) + +试卷答题界面如下。 + +![学生端-试卷](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/%E5%AD%A6%E7%94%9F%E7%AB%AF-%E8%AF%95%E5%8D%B7.jpg) + +## 启动 + +### 后端 + +我们这里以 PostgreSQL 数据库版本来演示。 + +#### 安装 PostgreSQL + +**这里我们使用 Docker 下载最近版的 PostgreSQL 镜像 ,默认大家已经安装了 Docker。** + +```bash +$ docker pull postgres +``` + +**查看 PostgreSQL 镜像:** + +```bash +$ docker images |grep postgres +postgres latest 62473370e7ee 2 weeks ago 314MB +``` + +**运行 PostgreSQL:** + +```bash +$ docker run -d -p 5432:5432 --name postgresql -e POSTGRES_PASSWORD=123456 postgres +``` + +#### 安装 Redis + +**这里我们使用 Docker 下载最近版的 Redis 镜像 ,默认大家已经安装了 Docker。** + +```bash +$ docker pull redis +``` + +**查看 Redis 镜像:** + +```bash +$ docker images |grep redis +``` + +**运行 Redis:** + +```bash +$ docker run -itd --name redis-test -p 6379:6379 redis +``` + +#### 创建数据库并执行数据库脚本 + +首先创建一个名字叫做`xzs` 的数据库,然后执行相应的数据库脚本即可(数据库脚本在 `uexam/source/xzs/sql` 目录下。)。 + +#### 配置文件修改 + +使用 IntelliJ IDEA 打开 `uexam/source/xzs` (后台代码),修改 `application-dev.yml` ,将 postgesql/mysql、redis 的服务地址改为自己本地的。 + +#### 启动项目 + +直接运行 `XzsApplication` 即可。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-9/image-20200903180710467.png) + +启动成功后,打开下面的链接即可跳转到对应的端: + +- 学生系统地址:http://localhost:8000/student +- 管理端地址:http://localhost:8000/admin + +**注意:这种方式,前端虽然也启动了,也能访问,不过是内嵌在后端项目中。如果如果我们需要前后端分离的话,需要单独运行前端项目** + +### 前端 + +小程序端的就不演示了,我这里只演示一下 web 端的。 + +web 端代码在 `uexam/source/vue` 下,我们需要首先进入这个目录,然后分别对 `xzs-admin` (管理端) 和 `xzs-student` (学生端)执行下面两个命令。 + +**1.下载相关依赖** + +```bash +$ npm install +``` + +**2.启动项目** + +```bash +$ npm run serve +``` + +启动完成之后,打开下面的链接即可跳转到对应的端: + +- 学生系统地址:http://localhost:8001 +- 管理端地址:http://localhost:8002 \ No newline at end of file From 55c934f63b5888187375c5974d447083c3493b72 Mon Sep 17 00:00:00 2001 From: guide Date: Fri, 1 Jan 2021 00:52:54 +0800 Subject: [PATCH 183/207] =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename "docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" => "docs/projects/kkFileView-SpringBoot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" (100%) diff --git a/README.md b/README.md index f4a2c81..47d9dd2 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ ## 实战项目 -1. [使用 Spring Boot搭建一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-基于Spring Boot在线文件预览系统.md) +1. [使用 Spring Boot搭建一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-SpringBoot在线文件预览系统.md) 2. [ SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC权限控制...](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247495011&idx=1&sn=f574f5d75c3720d8b2a665d1d5234d28&chksm=cea1a2a8f9d62bbe9f13f5a030893fe3da6956c4be41471513e6247f74cba5a8df9941798b6e&token=212861022&lang=zh_CN#rd) 3. [一个基于Spring Cloud 的面试刷题系统。](./docs/projects/SpringCloud刷题系统.md) 4. [一个基于 Spring Boot 的在线考试系统](./docs/projects/一个基于SpringBoot的在线考试系统.md) diff --git "a/docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" "b/docs/projects/kkFileView-SpringBoot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" similarity index 100% rename from "docs/projects/kkFileView-\345\237\272\344\272\216Spring Boot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" rename to "docs/projects/kkFileView-SpringBoot\345\234\250\347\272\277\346\226\207\344\273\266\351\242\204\350\247\210\347\263\273\347\273\237.md" From a6711f542994d352fcf2cccd6d7e967c4c970aef Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 2 Mar 2021 09:06:45 +0800 Subject: [PATCH 184/207] =?UTF-8?q?Delete=20SpringBoot=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot\351\235\242\350\257\225\351\242\230.md" | 625 ------------------ 1 file changed, 625 deletions(-) delete mode 100644 "docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" diff --git "a/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" "b/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 05f6eba..0000000 --- "a/docs/interview/SpringBoot\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,625 +0,0 @@ -市面上关于 Spring Boot 的面试题抄来抄去,毫无价值可言。 - -这篇文章,我会简单就自己这几年使用 Spring Boot 的一些经验,总结一些常见的面试题供小伙伴们自测和学习。少部分关于 Spring/Spring Boot 的介绍参考了官网,其他皆为原创。 - -### 1. 简单介绍一下 Spring?有啥缺点? - -Spring 是重量级企业开发框架 **Enterprise JavaBean(EJB)** 的替代品,Spring 为企业级 Java 开发提供了一种相对简单的方法,通过 **依赖注入** 和 **面向切面编程** ,用简单的 **Java 对象(Plain Old Java Object,POJO)** 实现了 EJB 的功能 - -**虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的(需要大量 XML 配置)** 。 - -为此,Spring 2.5 引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式 XML 配置。Spring 3.0 引入了基于 Java 的配置,这是一种类型安全的可重构配置方式,可以代替 XML。 - -尽管如此,我们依旧没能逃脱配置的魔爪。开启某些 Spring 特性时,比如事务管理和 Spring MVC,还是需要用 XML 或 Java 进行显式配置。启用第三方库时也需要显式配置,比如基于 Thymeleaf 的 Web 视图。配置 Servlet 和过滤器(比如 Spring 的`DispatcherServlet`)同样需要在 web.xml 或 Servlet 初始化代码里进行显式配置。组件扫描减少了配置量,Java 配置让它看上去简洁不少,但 Spring 还是需要不少配置。 - -光配置这些 XML 文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。 - -### 2. 为什么要有 SpringBoot? - -Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot F 旨在简化 Spring 开发(减少配置文件,开箱即用!)。 - -![why-we-need-springboot](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/why-we-need-springboot.png) - -### 3. 说出使用 Spring Boot 的主要优点 - -1. 开发基于 Spring 的应用程序很容易。 -2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。 -3. Spring Boot 不需要编写大量样板代码、XML 配置和注释。 -4. Spring 引导应用程序可以很容易地与 Spring 生态系统集成,如 Spring JDBC、Spring ORM、Spring Data、Spring Security 等。 -5. Spring Boot 遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。 -6. Spring Boot 应用程序提供嵌入式 HTTP 服务器,如 Tomcat 和 Jetty,可以轻松地开发和测试 web 应用程序。(这点很赞!普通运行 Java 程序的方式就能运行基于 Spring Boot web 项目,省事很多) -7. Spring Boot 提供命令行接口(CLI)工具,用于开发和测试 Spring Boot 应用程序,如 Java 或 Groovy。 -8. Spring Boot 提供了多种插件,可以使用内置工具(如 Maven 和 Gradle)开发和测试 Spring Boot 应用程序。 - -### 4. 什么是 Spring Boot Starters? - -Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。 - -举个例子:在没有 Spring Boot Starters 之前,我们开发 REST 服务或 Web 应用程序时; 我们需要使用像 Spring MVC,Tomcat 和 Jackson 这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发 REST 服务需要的所有依赖。 - -```xml - - org.springframework.boot - spring-boot-starter-web - -``` - -### 5. Spring Boot 支持哪些内嵌 Servlet 容器? - -Spring Boot 支持以下嵌入式 Servlet 容器: - -| **Name** | **Servlet Version** | -| ------------ | ------------------- | -| Tomcat 9.0 | 4.0 | -| Jetty 9.4 | 3.1 | -| Undertow 2.0 | 4.0 | - -您还可以将 Spring 引导应用程序部署到任何 Servlet 3.1+兼容的 Web 容器中。 - -这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。 - -### 6. 如何在 Spring Boot 应用程序中使用 Jetty 而不是 Tomcat? - -Spring Boot (`spring-boot-starter-web`)使用 Tomcat 作为默认的嵌入式 servlet 容器, 如果你想使用 Jetty 的话只需要修改`pom.xml`(Maven)或者`build.gradle`(Gradle)就可以了。 - -**Maven:** - -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - - org.springframework.boot - spring-boot-starter-jetty - -``` - -**Gradle:** - -```groovy -compile("org.springframework.boot:spring-boot-starter-web") { - exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' -} -compile("org.springframework.boot:spring-boot-starter-jetty") -``` - -说个题外话,从上面可以看出使用 Gradle 更加简洁明了,但是国内目前还是 Maven 使用的多一点,我个人觉得 Gradle 在很多方面都要好很多。 - -### 7. 介绍一下@SpringBootApplication 注解 - -```java -package org.springframework.boot.autoconfigure; -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Inherited -@SpringBootConfiguration -@EnableAutoConfiguration -@ComponentScan(excludeFilters = { - @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), - @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) -public @interface SpringBootApplication { - ...... -} -``` - -```java -package org.springframework.boot; -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Configuration -public @interface SpringBootConfiguration { - -} -``` - -可以看出大概可以把 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是: - -- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 -- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 -- `@Configuration`:允许在上下文中注册额外的 bean 或导入其他配置类 - -### 8. Spring Boot 的自动配置是如何实现的? - -这个是因为`@SpringBootApplication`注解的原因,在上一个问题中已经提到了这个注解。我们知道 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。 - -- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 -- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 -- `@Configuration`:允许在上下文中注册额外的 bean 或导入其他配置类 - -`@EnableAutoConfiguration`是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程): - -```java -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.springframework.context.annotation.Import; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Inherited -@AutoConfigurationPackage -@Import({AutoConfigurationImportSelector.class}) -public @interface EnableAutoConfiguration { - String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; - - Class[] exclude() default {}; - - String[] excludeName() default {}; -} -``` - -`@EnableAutoConfiguration` 注解通过 Spring 提供的 `@Import` 注解导入了`AutoConfigurationImportSelector`类(`@Import` 注解可以导入配置类或者 Bean 到当前类中)。 - -`AutoConfigurationImportSelector`类中`getCandidateConfigurations`方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。 - -```java - protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { - List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), - getBeanClassLoader()); - Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " - + "are using a custom packaging, make sure that file is correct."); - return configurations; - } -``` - -自动配置信息有了,那么自动配置还差什么呢? - -`@Conditional` 注解。`@ConditionalOnClass`(指定的类必须存在于类路径下),`@ConditionalOnBean`(容器中是否有指定的 Bean)等等都是对`@Conditional`注解的扩展。 - -拿 Spring Security 的自动配置举个例子:`SecurityAutoConfiguration`中导入了`WebSecurityEnablerConfiguration`类,`WebSecurityEnablerConfiguration`源代码如下: - -```java -@Configuration -@ConditionalOnBean(WebSecurityConfigurerAdapter.class) -@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@EnableWebSecurity -public class WebSecurityEnablerConfiguration { - -} -``` - -`WebSecurityEnablerConfiguration`类中使用`@ConditionalOnBean`指定了容器中必须还有`WebSecurityConfigurerAdapter` 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 `WebSecurityConfigurerAdapter`,这样自动将配置就完成了。 - -### 9. 开发 RESTful Web 服务常用的注解有哪些? - -**Spring Bean 相关:** - -- `@Autowired` : 自动导入对象到类中,被注入进的类同样要被 Spring 容器管理。 -- `@RestController` : `@RestController`注解是`@Controller和`@`ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。 -- `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 -- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 -- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 -- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 - -**处理常见的 HTTP 请求类型:** - -- `@GetMapping` : GET 请求、 -- `@PostMapping` : POST 请求。 -- `@PutMapping` : PUT 请求。 -- `@DeleteMapping` : DELETE 请求。 - -**前后端传值:** - -- `@RequestParam`以及`@Pathvairable :@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。` -- `@RequestBody` :用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且 Content-Type 为 `application/json` 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 - -详细介绍可以查看这篇文章:[《Spring/Spring Boot 常用注解总结》](https://snailclimb.gitee.io/javaguide/#/./docs/system-design/framework/spring/SpringBoot+Spring%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3%E6%80%BB%E7%BB%93?id=_21-autowired) 。 - -### 10. Spirng Boot 常用的两种配置文件 - -我们可以通过 `application.properties`或者 `application.yml` 对 Spring Boot 程序进行简单的配置。如果,你不进行配置的话,就是使用的默认配置。 - -### 11. 什么是 YAML?YAML 配置的优势在哪里 ? - -YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。 - -相比于 Properties 配置的方式,YAML 配置的方式更加直观清晰,简介明了,有层次感。 - -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/SpringBoot%E9%9D%A2%E8%AF%95%E9%A2%98/image-20201228202423406.png) - -但是,YAML 配置的方式有一个缺点,那就是不支持 `@PropertySource` 注解导入自定义的 YAML 配置。 - -### 12. Spring Boot 常用的读取配置文件的方法有哪些? - -我们要读取的配置文件`application.yml` 内容如下: - -```yaml -wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! - -my-profile: - name: Guide哥 - email: koushuangbwcx@163.com - -library: - location: 湖北武汉加油中国加油 - books: - - name: 天才基本法 - description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 - - name: 时间的秩序 - description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 - - name: 了不起的我 - description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? -``` - -#### 12.1. 通过 `@value` 读取比较简单的配置信息 - -使用 `@Value("${property}")` 读取比较简单的配置信息: - -```java -@Value("${wuhan2020}") -String wuhan2020; -``` - -> **需要注意的是 `@value`这种方式是不被推荐的,Spring 比较建议的是下面几种读取配置信息的方式。** - -#### 12.2. 通过`@ConfigurationProperties`读取并与 bean 绑定 - -> **`LibraryProperties` 类上加了 `@Component` 注解,我们可以像使用普通 bean 一样将其注入到类中使用。** - -```java - -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -@ConfigurationProperties(prefix = "library") -@Setter -@Getter -@ToString -class LibraryProperties { - private String location; - private List books; - - @Setter - @Getter - @ToString - static class Book { - String name; - String description; - } -} - -``` - -这个时候你就可以像使用普通 bean 一样,将其注入到类中使用: - -```java -package cn.javaguide.readconfigproperties; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author shuang.kou - */ -@SpringBootApplication -public class ReadConfigPropertiesApplication implements InitializingBean { - - private final LibraryProperties library; - - public ReadConfigPropertiesApplication(LibraryProperties library) { - this.library = library; - } - - public static void main(String[] args) { - SpringApplication.run(ReadConfigPropertiesApplication.class, args); - } - - @Override - public void afterPropertiesSet() { - System.out.println(library.getLocation()); - System.out.println(library.getBooks()); } -} -``` - -控制台输出: - -``` -湖北武汉加油中国加油 -[LibraryProperties.Book(name=天才基本法, description........] -``` - -#### 12.3. 通过`@ConfigurationProperties`读取并校验 - -我们先将`application.yml`修改为如下内容,明显看出这不是一个正确的 email 格式: - -```yaml -my-profile: - name: Guide哥 - email: koushuangbwcx@ -``` - -> **`ProfileProperties` 类没有加 `@Component` 注解。我们在我们要使用`ProfileProperties` 的地方使用`@EnableConfigurationProperties`注册我们的配置 bean:** - -```java -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.Email; -import javax.validation.constraints.NotEmpty; - -/** -* @author shuang.kou -*/ -@Getter -@Setter -@ToString -@ConfigurationProperties("my-profile") -@Validated -public class ProfileProperties { - @NotEmpty - private String name; - - @Email - @NotEmpty - private String email; - - //配置文件中没有读取到的话就用默认值 - private Boolean handsome = Boolean.TRUE; - -} -``` - -具体使用: - -```java -package cn.javaguide.readconfigproperties; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - -/** - * @author shuang.kou - */ -@SpringBootApplication -@EnableConfigurationProperties(ProfileProperties.class) -public class ReadConfigPropertiesApplication implements InitializingBean { - private final ProfileProperties profileProperties; - - public ReadConfigPropertiesApplication(ProfileProperties profileProperties) { - this.profileProperties = profileProperties; - } - - public static void main(String[] args) { - SpringApplication.run(ReadConfigPropertiesApplication.class, args); - } - - @Override - public void afterPropertiesSet() { - System.out.println(profileProperties.toString()); - } -} - -``` - -因为我们的邮箱格式不正确,所以程序运行的时候就报错,根本运行不起来,保证了数据类型的安全性: - -```visual basic -Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my-profile' to cn.javaguide.readconfigproperties.ProfileProperties failed: - - Property: my-profile.email - Value: koushuangbwcx@ - Origin: class path resource [application.yml]:5:10 - Reason: must be a well-formed email address -``` - -我们把邮箱测试改为正确的之后再运行,控制台就能成功打印出读取到的信息: - -``` -ProfileProperties(name=Guide哥, email=koushuangbwcx@163.com, handsome=true) -``` - -#### 12.4. `@PropertySource`读取指定的 properties 文件 - -```java -import lombok.Getter; -import lombok.Setter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.PropertySource; -import org.springframework.stereotype.Component; - -@Component -@PropertySource("classpath:website.properties") -@Getter -@Setter -class WebSite { - @Value("${url}") - private String url; -} -``` - -使用: - -```java -@Autowired -private WebSite webSite; - -System.out.println(webSite.getUrl());//https://javaguide.cn/ - -``` - -### 13. Spring Boot 加载配置文件的优先级了解么? - -Spring 读取配置文件也是有优先级的,直接上图: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/read-config-properties-priority.jpg) - -更对内容请查看官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config - -### 14. 常用的 Bean 映射工具有哪些? - -MapStruct、ModelMapper、Dozer、Orika、JMapper 是 5 种常用的 Bean 映射工具。 - -综合日常使用情况和相关测试数据,个人感觉 MapStruct、ModelMapper 这两个 Bean 映射框架是最佳选择。 - -### 15. Spring Boot 如何监控系统实际运行状况? - -我们可以使用 Spring Boot Actuator 来对 Spring Boot 项目进行简单的监控。 - -```xml - - org.springframework.boot - spring-boot-starter-actuator - -``` - -集成了这个模块之后,你的 Spring Boot 应用程序就自带了一些开箱即用的获取程序运行时的内部状态信息的 API。 - -比如通过 GET 方法访问 `/health` 接口,你就可以获取应用程序的健康指标。 - -### 16. Spring Boot 如何做请求参数校验? - -数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。 - -Spring Boot 程序做请求参数校验的话只需要`spring-boot-starter-web` 依赖就够了,它的子依赖包含了我们所需要的东西。 - -#### 16.1. 校验注解 - -**JSR 提供的校验注解**: - -- `@Null` 被注释的元素必须为 null -- `@NotNull` 被注释的元素必须不为 null -- `@AssertTrue` 被注释的元素必须为 true -- `@AssertFalse` 被注释的元素必须为 false -- `@Min(value)` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@Max(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@DecimalMin(value)` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@Size(max=, min=)` 被注释的元素的大小必须在指定的范围内 -- `@Digits (integer, fraction)` 被注释的元素必须是一个数字,其值必须在可接受的范围内 -- `@Past` 被注释的元素必须是一个过去的日期 -- `@Future` 被注释的元素必须是一个将来的日期 -- `@Pattern(regex=,flag=)` 被注释的元素必须符合指定的正则表达式 - -**Hibernate Validator 提供的校验注解**: - -- `@NotBlank(message =)` 验证字符串非 null,且长度必须大于 0 -- `@Email` 被注释的元素必须是电子邮箱地址 -- `@Length(min=,max=)` 被注释的字符串的大小必须在指定的范围内 -- `@NotEmpty` 被注释的字符串的必须非空 -- `@Range(min=,max=,message=)` 被注释的元素必须在合适的范围内 - -**使用示例:** - -```java -@Data -@AllArgsConstructor -@NoArgsConstructor -public class Person { - - @NotNull(message = "classId 不能为空") - private String classId; - - @Size(max = 33) - @NotNull(message = "name 不能为空") - private String name; - - @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围") - @NotNull(message = "sex 不能为空") - private String sex; - - @Email(message = "email 格式不正确") - @NotNull(message = "email 不能为空") - private String email; - -} -``` - -#### 16.2. 验证请求体(RequestBody) - -我们在需要验证的参数上加上了`@Valid` 注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。 - -```java -@RestController -@RequestMapping("/api") -public class PersonController { - - @PostMapping("/person") - public ResponseEntity getPerson(@RequestBody @Valid Person person) { - return ResponseEntity.ok().body(person); - } -} -``` - -#### 16.3. 验证请求参数(Path Variables 和 Request Parameters) - -一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。 - -```java -@RestController -@RequestMapping("/api") -@Validated -public class PersonController { - - @GetMapping("/person/{id}") - public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { - return ResponseEntity.ok().body(id); - } - - @PutMapping("/person") - public ResponseEntity getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) { - return ResponseEntity.ok().body(name); - } -} -``` - -更多内容请参考我的原创: **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://snailclimb.gitee.io/springboot-guide/#/./docs/advanced/spring-bean-validation)** - -### 17. 如何使用 Spring Boot 实现全局异常处理? - -可以使用 `@ControllerAdvice` 和 `@ExceptionHandler` 处理全局异常。 - -更多内容请参考我的原创 :[Spring Boot 异常处理在实际项目中的应用](https://snailclimb.gitee.io/springboot-guide/#/./docs/advanced/springboot-handle-exception-plus) - -### 18. Spring Boot 中如何实现定时任务 ? - -我们使用 `@Scheduled` 注解就能很方便地创建一个定时任务。 - -```java -@Component -public class ScheduledTasks { - private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); - private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); - - /** - * fixedRate:固定速率执行。每5秒执行一次。 - */ - @Scheduled(fixedRate = 5000) - public void reportCurrentTimeWithFixedRate() { - log.info("Current Thread : {}", Thread.currentThread().getName()); - log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date())); - } -} -``` - -单纯依靠 `@Scheduled` 注解 还不行,我们还需要在 SpringBoot 中我们只需要在启动类上加上`@EnableScheduling` 注解,这样才可以启动定时任务。`@EnableScheduling` 注解的作用是发现注解 `@Scheduled` 的任务并在后台执行该任务。 \ No newline at end of file From 5dab6427dede7369fb995e1bf690fd29f1e92586 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 2 Mar 2021 09:06:47 +0800 Subject: [PATCH 185/207] Update README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 47d9dd2..68e0616 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,6 @@ 3. [一个基于Spring Cloud 的面试刷题系统。](./docs/projects/SpringCloud刷题系统.md) 4. [一个基于 Spring Boot 的在线考试系统](./docs/projects/一个基于SpringBoot的在线考试系统.md) -## 面试题 - -- [几道简单的 SpringBoot面试题](./docs/interview/SpringBoot面试题.md) -- [RestController VS Controller](./docs/basis/RestControllerVSController.md) - ## 说明 1. 项目 logo 由 [logoly](https://logoly.pro/#/) 生成。 From a2b22aca88282077b25b01d2857645629e7eaaad Mon Sep 17 00:00:00 2001 From: fengbaichao Date: Sun, 18 Apr 2021 10:41:20 +0800 Subject: [PATCH 186/207] =?UTF-8?q?=E4=BD=BF=E7=94=A8PowerMockRunner?= =?UTF-8?q?=E5=92=8CMockito=E7=BC=96=E5=86=99=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E4=BE=8B=E8=AF=A6=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: fengbaichao --- docs/basis/PowerMockRunnerAndMockito.md | 177 ++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/basis/PowerMockRunnerAndMockito.md diff --git a/docs/basis/PowerMockRunnerAndMockito.md b/docs/basis/PowerMockRunnerAndMockito.md new file mode 100644 index 0000000..dbc2b0a --- /dev/null +++ b/docs/basis/PowerMockRunnerAndMockito.md @@ -0,0 +1,177 @@ +单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在Spring框架中常用的两种测试框架:PowerMockRunner和SpringRunner两个单元测试,鉴于SpringRunner启动的一系列依赖和数据连接的问题,推荐使用PowerMockRunner,这样能有效的提高测试的效率,并且其提供的API能覆盖的场景广泛,使用方便,可谓是Java单元测试之模拟利器。 + +#### 1. PowerMock是什么? + +PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用PowerMock,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可让您更轻松地访问对象的内部状态。 + + +举个例子,你在使用Junit进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用PowerMock,拦截数据库操作,并模拟返回参数。 + +#### 2. PowerMock包引入 + +```xml + + + org.powermock + powermock-core + 2.0.2 + test + + +org.mockito +mockito-core +2.23.0 + + +org.powermock +powermock-module-junit4 +2.0.4 +test + + +org.powermock +powermock-api-mockito2 +2.0.2 +test + + +com.github.jsonzou +jmockdata +4.3.0 + + +``` + +#### 3. 重要注解说明 + +```java +@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试 +@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类 +@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误 +``` + + + +#### 4. 使用示例 + +##### 4.1 模拟接口返回 + +首先对接口进行mock,然后录制相关行为 + +```java +InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class) + + Powermockito.when(mock.method(Params…)).thenReturn(value) + + Powermockito.when(mock.method(Params..)).thenThrow(Exception) +``` + +##### 4.2 设置对象的private属性 + +需要使用whitebox向class或者对象中赋值。 + +如我们已经对接口尽心了mock,现在需要将此mock加入到对象中,可以采用如下方法: + +```java +Whitebox.setInternalState(Object object, String fieldname, Object… value); +``` + +其中object为需要设置属性的静态类或对象。 + +##### 4.3 模拟构造函数 + +对于模拟构造函数,也即当出现new InstanceClass()时可以将此构造函数拦截并替换结果为我们需要的mock对象。 + +注意:使用时需要加入标记: + +```java +@RunWith(PowerMockRunner.class) + +@PrepareForTest({ InstanceClass.class }) + +@PowerMockIgnore("javax.management.\*") + +Powermockito.whenNew(InstanceClass.class).thenReturn(Object value) +``` + +##### 4.4 模拟静态方法 + +模拟静态方法类似于模拟构造函数,也需要加入注释标记。 + +```java +@RunWith(PowerMockRunner.class) + +@PrepareForTest({ StaticClassToMock.class }) + +@PowerMockIgnore("javax.management.\*") + +Powermockito.mockStatic(StaticClassToMock.class); + + Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) +``` + +##### 4.5 模拟final方法 + +Final方法的模拟类似于模拟静态方法。 + +```java +@RunWith(PowerMockRunner.class) + +@PrepareForTest({ FinalClassToMock.class }) + +@PowerMockIgnore("javax.management.\*") + +Powermockito.mockStatic(FinalClassToMock.class); + + Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) +``` + +##### 4.6 模拟静态类 + +模拟静态类类似于模拟静态方法。 + +##### 4.7 使用spy方法避免执行被测类中的成员函数 + +如被测试类为:TargetClass,想要屏蔽的方法为targetMethod. + +```java +1) PowerMockito.spy(TargetClass.class); + + 2) Powemockito.when(TargetClass.targetMethod()).doReturn() + + 3) 注意加入 + +@RunWith(PowerMockRunner.class) + +@PrepareForTest(DisplayMoRelationBuilder.class) + +@PowerMockIgnore("javax.management.*") +``` + +##### 4.8 参数匹配器 + +有时我们在处理doMethod(Param param)时,不想进行精确匹配,这时可以使用Mockito提供的模糊匹配方式。 + +如:Mockito.anyInt(),Mockito.anyString() + +##### 4.9 处理public void型的静态方法 + +```java +Powermockito.doNothing.when(T class2mock, String method, … params> +``` + +#### 5. 单元测试用例可选清单 + +输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。 + +- 必传项测试 +- 唯一字段值测试 +- 空值测试 +- 字段只接受允许的字符 +- 负值测试 +- 字段限于字段长度规范 +- 不可能的值 +- 垃圾值测试 +- 检查字段之间的依赖性 +- 等效类划分和边界条件测试 +- 错误和异常处理测试 \ No newline at end of file From 88b764a6e806b28cfa10617e616c019a18415026 Mon Sep 17 00:00:00 2001 From: guide Date: Mon, 19 Apr 2021 16:30:40 +0800 Subject: [PATCH 187/207] Update SpringBoot-ScheduleTasks.md --- docs/advanced/SpringBoot-ScheduleTasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/SpringBoot-ScheduleTasks.md b/docs/advanced/SpringBoot-ScheduleTasks.md index 135eb2d..a443545 100644 --- a/docs/advanced/SpringBoot-ScheduleTasks.md +++ b/docs/advanced/SpringBoot-ScheduleTasks.md @@ -12,7 +12,7 @@ > Cron 表达式: 主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。 > -> 推荐一个在线Cron表达式生成器:[http://cron.qqe2.com/](http://cron.qqe2.com/) +> 推荐一个在线Cron表达式生成器: [https://crontab.guru/](https://crontab.guru/) ```java import org.slf4j.Logger; From 0dffa1aca96c9c8148f59049968960ca963d99ae Mon Sep 17 00:00:00 2001 From: fengbaichao Date: Mon, 19 Apr 2021 23:56:50 +0800 Subject: [PATCH 188/207] =?UTF-8?q?=E9=80=9A=E8=BF=87=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E5=88=86=E6=9E=90Spring=20Security=E7=94=A8=E6=88=B7=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...g-security-login-authentication-process.md | 196 ++++++++++++++++++ docs/basis/PowerMockRunnerAndMockito.md | 8 +- 2 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 docs/advanced/spring-security-login-authentication-process.md diff --git a/docs/advanced/spring-security-login-authentication-process.md b/docs/advanced/spring-security-login-authentication-process.md new file mode 100644 index 0000000..89d0328 --- /dev/null +++ b/docs/advanced/spring-security-login-authentication-process.md @@ -0,0 +1,196 @@ +Spring Security的登录主要是由一系列的过滤器组成,我们如果需要修改登录的校验逻辑,只需要在过滤器链路上添加修改相关的逻辑即可。这里主要通过Spring Security的源码来了解相关的认证登录的逻辑。 + +#### 1.Spring Security的认证流程 + +主要分析: + +1. 认证用户的流程 +2. 如何进行认证校验 +3. 认证成功后怎么获取用户信息 + +具体的过滤器链路如下所示: + +[![cT2G4g.png](https://z3.ax1x.com/2021/04/19/cT2G4g.png)](https://imgtu.com/i/cT2G4g) + +Spring Security的认证流程图如下,认证的主要过程有: + +1. 用户提交用户名和密码,然后通过UsernamePasswordAuthenticationFilter对其进行封装成为UsernamePasswordAuthenticationToken对象,这个是AbstractAuthenticationToken的子类,而AbstractAuthenticationToken又是Authentication的一个实现,所以可以看到后续获取的都是Authentication类型的对象实例; +2. 将第一步的UsernamePasswordAuthenticationToken对象传递给AuthenticationManager; +3. 通过AbstractUserDetailsAuthenticationProvider的默认实现类DaoAuthenticationProvider的retrieveUser方法,这个方法会调用UserDetailsService的loadUserByUsername方法来进行用户名和密码的判断,使用的默认的逻辑进行处理; +4. 将成功认证后的用户信息放入到SecurityContextHolder中,之后可以通过SecurityContext获取用户的相关信息。 + +[![coGpvR.png](https://z3.ax1x.com/2021/04/19/coGpvR.png)](https://imgtu.com/i/coGpvR) + +spring-security源码下载地址: + +```java +https://github.com/spring-projects/spring-security +``` + +#### 2.Spring Security的认证源码分析 + +##### 2.1 搭建项目并访问 + +首先我们搭建一个Spring Security的项目,使用Spring Boot可以很方便的进行集成开发,主要引入如下的依赖即可(当然也可以查看官网,选择合适的版本): + +```java + + org.springframework.boot + spring-boot-starter-security + +``` + +启动项目后会随机生成一个密码串,这里需要复制保存以便登录的时候使用: + +[![coJ0ld.png](https://z3.ax1x.com/2021/04/19/coJ0ld.png)](https://imgtu.com/i/coJ0ld) + +访问登录地址: + +```java +http://localhost:8080/login +``` + +[![coJfpQ.png](https://z3.ax1x.com/2021/04/19/coJfpQ.png)](https://imgtu.com/i/coJfpQ) + +默认的账户名和密码: + +```java +账户名: user +密码: 项目启动时生成的密码串 +``` + +##### 2.2 进行源码分析 + +1. 进行断点后会发现首先进入的是UsernamePasswordAuthenticationFilter的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,会对用户名和密码进行封装成UsernamePasswordAuthenticationToken对象,然后调用this.getAuthenticationManager().authenticate(authRequest)方法进入到AuthenticationManager中。 + + attemptAuthentication方法源码如下所示: + +```java +@Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { + if (this.postOnly && !request.getMethod().equals("POST")) { + throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); + } + String username = obtainUsername(request); + username = (username != null) ? username : ""; + username = username.trim(); + String password = obtainPassword(request); + password = (password != null) ? password : ""; + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); + // Allow subclasses to set the "details" property + setDetails(request, authRequest); + return this.getAuthenticationManager().authenticate(authRequest); + } +``` + +2. 随后请求进入到WebSecurityConfigurerAdapter的AuthenticationManagerDelegator中,AuthenticationManagerDelegator是AuthenticationManager的一个子类,最后封装成为UsernamePasswordAuthenticationToken对象,供DaoAuthenticationProvider使用。 + + AuthenticationManagerDelegator的源码如下: + + ```java + static final class AuthenticationManagerDelegator implements AuthenticationManager { + private AuthenticationManagerBuilder delegateBuilder; + private AuthenticationManager delegate; + private final Object delegateMonitor = new Object(); + private Set beanNames; + + AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) { + Assert.notNull(delegateBuilder, "delegateBuilder cannot be null"); + Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager"); + ReflectionUtils.makeAccessible(parentAuthMgrField); + this.beanNames = getAuthenticationManagerBeanNames(context); + validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames); + this.delegateBuilder = delegateBuilder; + } + + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (this.delegate != null) { + return this.delegate.authenticate(authentication); + } else { + synchronized(this.delegateMonitor) { + if (this.delegate == null) { + this.delegate = (AuthenticationManager)this.delegateBuilder.getObject(); + this.delegateBuilder = null; + } + } + + return this.delegate.authenticate(authentication); + } + } + + private static Set getAuthenticationManagerBeanNames(ApplicationContext applicationContext) { + String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class); + return new HashSet(Arrays.asList(beanNamesForType)); + } + + private static void validateBeanCycle(Object auth, Set beanNames) { + if (auth != null && !beanNames.isEmpty() && auth instanceof Advised) { + TargetSource targetSource = ((Advised)auth).getTargetSource(); + if (targetSource instanceof LazyInitTargetSource) { + LazyInitTargetSource lits = (LazyInitTargetSource)targetSource; + if (beanNames.contains(lits.getTargetBeanName())) { + throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication."); + } + } + } + } + } + ``` + + org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration.AuthenticationManagerDelegator#authenticate + + ```java + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (this.delegate != null) { + return this.delegate.authenticate(authentication); + } + synchronized (this.delegateMonitor) { + if (this.delegate == null) { + this.delegate = this.delegateBuilder.getObject(); + this.delegateBuilder = null; + } + } + return this.delegate.authenticate(authentication); + } + ``` + +3. 进入到DaoAuthenticationProvider的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法进行用户的认证,这里的认证主要会调用默认的UserDetailsService对用户名和密码进行校验,如果是使用的类似于Mysql的数据源,其默认的实现是JdbcDaoImpl。 + + org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser + + ```java + @Override + protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + prepareTimingAttackProtection(); + try { + UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); + if (loadedUser == null) { + throw new InternalAuthenticationServiceException( + "UserDetailsService returned null, which is an interface contract violation"); + } + return loadedUser; + } + catch (UsernameNotFoundException ex) { + mitigateAgainstTimingAttack(authentication); + throw ex; + } + catch (InternalAuthenticationServiceException ex) { + throw ex; + } + catch (Exception ex) { + throw new InternalAuthenticationServiceException(ex.getMessage(), ex); + } + } + + ``` + +4. 将上一步认证后的用户实例放入SecurityContextHolder中,至此我们可以很方便的从SecurityContextHolder中获取用户信息,方法如下: + + ```java + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + ``` + + diff --git a/docs/basis/PowerMockRunnerAndMockito.md b/docs/basis/PowerMockRunnerAndMockito.md index dbc2b0a..d5a7b12 100644 --- a/docs/basis/PowerMockRunnerAndMockito.md +++ b/docs/basis/PowerMockRunnerAndMockito.md @@ -61,9 +61,9 @@ PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无 ```java InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class) - Powermockito.when(mock.method(Params…)).thenReturn(value) +Powermockito.when(mock.method(Params…)).thenReturn(value) - Powermockito.when(mock.method(Params..)).thenThrow(Exception) +Powermockito.when(mock.method(Params..)).thenThrow(Exception) ``` ##### 4.2 设置对象的private属性 @@ -137,9 +137,9 @@ Powermockito.mockStatic(FinalClassToMock.class); ```java 1) PowerMockito.spy(TargetClass.class); - 2) Powemockito.when(TargetClass.targetMethod()).doReturn() +2) Powemockito.when(TargetClass.targetMethod()).doReturn() - 3) 注意加入 +3) 注意加入 @RunWith(PowerMockRunner.class) From ce64c42252e6889b046971b9248ce4f2b7da3cbe Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 21:45:14 +0800 Subject: [PATCH 189/207] =?UTF-8?q?[feat]SpringBoot=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- docs/advanced/spring-bean-validation.md | 574 ++++++++++-------- .../advanced/bean-validation-demo/pom.xml | 13 +- .../controller/PersonController.java | 17 +- .../beanvalidationdemo/entity/Person.java | 33 +- .../entity/PersonRequest.java | 44 ++ .../service/PersonService.java | 3 +- .../validation/PhoneNumber.java | 2 + .../validation/PhoneNumberValidator.java | 9 +- .../HelloWorldControllerTest.java | 28 - .../PersonControllerTest.java | 76 +-- .../beanvalidationdemo/PersonServiceTest.java | 23 +- 12 files changed, 412 insertions(+), 416 deletions(-) create mode 100644 source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java delete mode 100644 source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/HelloWorldControllerTest.java diff --git a/README.md b/README.md index 68e0616..e13f958 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ ### 进阶 1. Bean映射工具 :[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md) 、[5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md) -3. **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](./docs/advanced/spring-bean-validation.md)** +3. [如何在 Spring/Spring Boot 中做参数校验?](./docs/advanced/spring-bean-validation.md) 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) -5. **[新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)** -7. **[Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka)** +5. [新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md) +7. [Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka) 8. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) 9. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) diff --git a/docs/advanced/spring-bean-validation.md b/docs/advanced/spring-bean-validation.md index 902220a..e7a350d 100644 --- a/docs/advanced/spring-bean-validation.md +++ b/docs/advanced/spring-bean-validation.md @@ -1,142 +1,157 @@ -**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。** +数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。 -本文结合自己在项目中的实际使用经验,可以说**文章介绍的内容很实用,不了解的朋友可以学习一下,后面可以立马实践到项目上去。** +最普通的做法就像下面这样。我们通过 `if/else` 语句对请求的每一个参数一一校验。 -下面我会通过实例程序演示如何在 Java 程序中尤其是 Spring 程序中优雅地的进行参数验证。 - -## 基础设施搭建 +```java +@RestController +@RequestMapping("/api/person") +public class PersonController { -### 相关依赖 + @PostMapping + public ResponseEntity save(@RequestBody PersonRequest personRequest) { + if (personRequest.getClassId() == null + || personRequest.getName() == null + || !Pattern.matches("(^Man$|^Woman$|^UGM$)", personRequest.getSex())) { -如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖: - -```xml - - org.hibernate.validator - hibernate-validator - 6.0.9.Final - - - javax.el - javax.el-api - 3.0.0 - - - org.glassfish.web - javax.el - 2.2.6 - + } + return ResponseEntity.ok().body(personRequest); + } +} ``` -使用 Spring Boot 程序的话只需要`spring-boot-starter-web` 就够了,它的子依赖包含了我们所需要的东西。除了这个依赖,下面的演示还用到了 lombok ,所以不要忘记添加上相关依赖。 - -```xml - - - org.springframework.boot - spring-boot-starter-web - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - -``` +这样的代码,小伙伴们在日常开发中一定不少见,很多开源项目都是这样对请求入参做校验的。 -### 实体类 +但是,不太建议这样来写,这样的代码明显违背了 **单一职责原则**。大量的非业务代码混杂在业务代码中,非常难以维护,还会导致业务层代码冗杂! -下面这个是示例用到的实体类。 +实际上,我们是可以通过一些简单的手段对上面的代码进行改进的!这也是本文主要要介绍的内容! -```java -@Data -@AllArgsConstructor -@NoArgsConstructor -public class Person { +废话不多说!下面我会结合自己在项目中的实际使用经验,通过实例程序演示如何在 SpringBoot 程序中优雅地的进行参数验证(普通的 Java 程序同样适用)。 - @NotNull(message = "classId 不能为空") - private String classId; +不了解的朋友一定要好好看一下,学完马上就可以实践到项目上去。 - @Size(max = 33) - @NotNull(message = "name 不能为空") - private String name; +并且,本文实例项目使用的是目前最新的 Spring Boot 版本 2.4.5!(截止到 2021-04-21) - @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围") - @NotNull(message = "sex 不能为空") - private String sex; +## 添加相关依赖 - @Email(message = "email 格式不正确") - @NotNull(message = "email 不能为空") - private String email; +如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖: -} +```xml + + org.hibernate.validator + hibernate-validator + 6.0.9.Final + + + javax.el + javax.el-api + 3.0.0 + + + org.glassfish.web + javax.el + 2.2.6 + ``` -> 正则表达式说明: -> -> ``` -> - ^string : 匹配以 string 开头的字符串 -> - string$ :匹配以 string 结尾的字符串 -> - ^string$ :精确匹配 string 字符串 -> - ((^Man$|^Woman$|^UGM$)) : 值只能在 Man,Woman,UGM 这三个值中选择 -> ``` +不过,相信大家都是使用的 Spring Boot 框架来做开发。 -下面这部分校验注解说明内容参考自:https://www.cnkirito.moe/spring-validation/ ,感谢@[徐靖峰](https://github.com/lexburner)。 +基于 Spring Boot 的话,就比较简单了,只需要给项目添加上 `spring-boot-starter-web` 依赖就够了,它的子依赖包含了我们所需要的东西。另外,我们的示例项目中还使用到了 Lombok。 -**JSR提供的校验注解**: +![](https://img-blog.csdnimg.cn/20210421172058450.png) +```xml + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + junit + junit + 4.13.1 + test + + + org.springframework.boot + spring-boot-starter-test + test + + +``` -- `@Null` 被注释的元素必须为 null -- `@NotNull` 被注释的元素必须不为 null -- `@AssertTrue` 被注释的元素必须为 true -- `@AssertFalse` 被注释的元素必须为 false -- `@Min(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@Max(value) ` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@DecimalMin(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@Size(max=, min=) ` 被注释的元素的大小必须在指定的范围内 -- `@Digits (integer, fraction) ` 被注释的元素必须是一个数字,其值必须在可接受的范围内 -- `@Past ` 被注释的元素必须是一个过去的日期 -- `@Future` 被注释的元素必须是一个将来的日期 -- `@Pattern(regex=,flag=) ` 被注释的元素必须符合指定的正则表达式 - +但是!!! Spring Boot 2.3 1 之后,`spring-boot-starter-validation` 已经不包括在了 `spring-boot-starter-web` 中,需要我们手动加上! -**Hibernate Validator提供的校验注解**: +![](https://img-blog.csdnimg.cn/20210421170846695.png) +```xml + + org.springframework.boot + spring-boot-starter-validation + +``` -- `@NotBlank(message =) ` 验证字符串非null,且长度必须大于0 -- `@Email` 被注释的元素必须是电子邮箱地址 -- `@Length(min=,max=) ` 被注释的字符串的大小必须在指定的范围内 -- `@NotEmpty ` 被注释的字符串的必须非空 -- `@Range(min=,max=,message=)` 被注释的元素必须在合适的范围内 +## 验证 Controller 的输入 -## 验证Controller的输入 +### 验证请求体 -### 验证请求体(RequestBody) +验证请求体即使验证被 `@RequestBody` 注解标记的方法参数。 -**Controller:** +**`PersonController`** -我们在需要验证的参数上加上了`@Valid`注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。 +我们在需要验证的参数上加上了`@Valid`注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。 ```java - @RestController -@RequestMapping("/api") +@RequestMapping("/api/person") +@Validated public class PersonController { - @PostMapping("/person") - public ResponseEntity getPerson(@RequestBody @Valid Person person) { - return ResponseEntity.ok().body(person); + @PostMapping + public ResponseEntity save(@RequestBody @Valid PersonRequest personRequest) { + return ResponseEntity.ok().body(personRequest); } } ``` -**ExceptionHandler:** +**`PersonRequest`** + +我们使用校验注解对请求的参数进行校验! + +```java +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PersonRequest { + + @NotNull(message = "classId 不能为空") + private String classId; + + @Size(max = 33) + @NotNull(message = "name 不能为空") + private String name; + + @Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围") + @NotNull(message = "sex 不能为空") + private String sex; + +} + +``` + +正则表达式说明: + +- `^string` : 匹配以 string 开头的字符串 +- `string$` :匹配以 string 结尾的字符串 +- `^string$` :精确匹配 string 字符串 +- `(^Man$|^Woman$|^UGM$)` : 值只能在 Man,Woman,UGM 这三个值中选择 + +**`GlobalExceptionHandler`** 自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。如果对于下面的处理异常的代码不太理解的话,可以查看这篇文章 [《SpringBoot 处理异常的几种常见姿势》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485568&idx=2&sn=c5ba880fd0c5d82e39531fa42cb036ac&chksm=cea2474bf9d5ce5dcbc6a5f6580198fdce4bc92ef577579183a729cb5d1430e4994720d59b34&token=1924773784&lang=zh_CN#rd)。 @@ -157,14 +172,11 @@ public class GlobalExceptionHandler { } ``` -**通过测试验证:** - -下面我通过 MockMvc 模拟请求 Controller 的方式来验证是否生效,当然你也可以通过 Postman 这种工具来验证。 +**通过测试验证** -我们试一下所有参数输入正确的情况。 +下面我通过 `MockMvc` 模拟请求 `Controller` 的方式来验证是否生效。当然了,你也可以通过 `Postman` 这种工具来验证。 ```java -@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class PersonControllerTest { @@ -173,109 +185,93 @@ public class PersonControllerTest { @Autowired private ObjectMapper objectMapper; - + /** + * 验证出现参数不合法的情况抛出异常并且可以正确被捕获 + */ @Test - public void should_get_person_correctly() throws Exception { - Person person = new Person(); - person.setName("SnailClimb"); - person.setSex("Man"); - person.setClassId("82938390"); - person.setEmail("Snailclimb@qq.com"); - - mockMvc.perform(post("/api/person") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(person))) - .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb")) - .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390")) - .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man")) - .andExpect(MockMvcResultMatchers.jsonPath("email").value("Snailclimb@qq.com")); - } -} -``` - -验证出现参数不合法的情况抛出异常并且可以正确被捕获。 - -```java - @Test public void should_check_person_value() throws Exception { - Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); - - mockMvc.perform(post("/api/person") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(person))) + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + mockMvc.perform(post("/api/personRequest") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(personRequest))) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围")) - .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空")) - .andExpect(MockMvcResultMatchers.jsonPath("email").value("email 格式不正确")); + .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空")); } +} ``` -使用 Postman 验证结果如下: +**使用 `Postman` 验证** + +![](https://img-blog.csdnimg.cn/20210421175345253.png) -![Postman 验证结果](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/postman-validation.png) +### 验证请求参数 -### 验证请求参数(Path Variables 和 Request Parameters) +验证请求参数(Path Variables 和 Request Parameters)即是验证被 `@PathVariable` 以及 `@RequestParam` 标记的方法参数。 -**Controller:** +**`PersonController`** **一定一定不要忘记在类上加上 `Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。** ```java @RestController -@RequestMapping("/api") +@RequestMapping("/api/persons") @Validated public class PersonController { - @GetMapping("/person/{id}") - public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { + @GetMapping("/{id}") + public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) { return ResponseEntity.ok().body(id); } - @PutMapping("/person") - public ResponseEntity getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) { + @PutMapping + public ResponseEntity getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) { return ResponseEntity.ok().body(name); } } - ``` -**ExceptionHandler:** +**`ExceptionHandler`** ```java - @ExceptionHandler(ConstraintViolationException.class) - ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } + @ExceptionHandler(ConstraintViolationException.class) + ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } ``` -**通过测试验证:** +**通过测试验证** ```java - @Test - public void should_check_param_value() throws Exception { +@Test +public void should_check_path_variable() throws Exception { + mockMvc.perform(get("/api/person/6") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("getPersonByID.id: 超过 id 的范围了")); +} - mockMvc.perform(get("/api/person/6") - .contentType(MediaType.APPLICATION_JSON_UTF8)) - .andExpect(status().isBadRequest()) - .andExpect(content().string("getPersonByID.id: 超过 id 的范围了")); - } +@Test +public void should_check_request_param_value2() throws Exception { + mockMvc.perform(put("/api/person") + .param("name", "snailclimbsnailclimb") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("getPersonByName.name: 超过 name 的范围了")); +} +``` - @Test - public void should_check_param_value2() throws Exception { +**使用 `Postman` 验证** - mockMvc.perform(put("/api/person") - .param("name","snailclimbsnailclimb") - .contentType(MediaType.APPLICATION_JSON_UTF8)) - .andExpect(status().isBadRequest()) - .andExpect(content().string("getPersonByName.name: 超过 name 的范围了")); - } -``` +![](https://img-blog.csdnimg.cn/20210421190508416.png) + +![](https://img-blog.csdnimg.cn/20210421190810975.png) ## 验证 Service 中的方法 -我们还可以验证任何Spring组件的输入,而不是验证控制器级别的输入,我们可以使用`@Validated`和`@Valid`注释的组合来实现这一需求。 +我们还可以验证任何 Spring Bean 的输入,而不仅仅是 `Controller` 级别的输入。通过使用`@Validated`和`@Valid`注释的组合即可实现这一需求! + +一般情况下,我们在项目中也更倾向于使用这种方案。 **一定一定不要忘记在类上加上 `Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。** @@ -284,9 +280,10 @@ public class PersonController { @Validated public class PersonService { - public void validatePerson(@Valid Person person){ + public void validatePersonRequest(@Valid PersonRequest personRequest) { // do something } + } ``` @@ -295,53 +292,60 @@ public class PersonService { ```java @RunWith(SpringRunner.class) @SpringBootTest -@AutoConfigureMockMvc public class PersonServiceTest { @Autowired private PersonService service; - @Test(expected = ConstraintViolationException.class) - public void should_throw_exception_when_person_is_not_valid() { - Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); - service.validatePerson(person); + @Test + public void should_throw_exception_when_person_request_is_not_valid() { + try { + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + service.validatePersonRequest(personRequest); + } catch (ConstraintViolationException e) { + // 输出异常信息 + e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); + } } - } ``` +输出结果如下: + +``` +name 不能为空 +sex 值不在可选范围 +``` + ## Validator 编程方式手动进行参数验证 某些场景下可能会需要我们手动校验并获得校验结果。 +我们通过 `Validator` 工厂类获得的 `Validator` 示例。另外,如果是在 Spring Bean 中的话,还可以通过 `@Autowired` 直接注入的方式。 + ```java - @Test - public void check_person_manually() { - - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - Validator validator = factory.getValidator(); - Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); - Set> violations = validator.validate(person); - //output: - //email 格式不正确 - //name 不能为空 - //sex 值不在可选范围 - for (ConstraintViolation constraintViolation : violations) { - System.out.println(constraintViolation.getMessage()); - } - } +@Autowired +Validator validate ``` -上面我们是通过 `Validator` 工厂类获得的 `Validator` 示例,当然你也可以通过 `@Autowired` 直接注入的方式。但是在非 Spring Component 类中使用这种方式的话,只能通过工厂类来获得 `Validator`。 +具体使用情况如下: ```java -@Autowired -Validator validate +ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); +Validator validator = factory.getValidator() +PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); +Set> violations = validator.validate(personRequest); +// 输出异常信息 +violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); +} +``` + +输出结果如下: + +``` +sex 值不在可选范围 +name 不能为空 ``` ## 自定以 Validator(实用) @@ -350,9 +354,9 @@ Validator validate ### 案例一:校验特定字段的值是否在可选范围 -比如我们现在多了这样一个需求:Person类多了一个 region 字段,region 字段只能是`China`、`China-Taiwan`、`China-HongKong`这三个中的一个。 +比如我们现在多了这样一个需求:`PersonRequest` 类多了一个 `Region` 字段,`Region` 字段只能是`China`、`China-Taiwan`、`China-HongKong`这三个中的一个。 -第一步你需要创建一个注解: +**第一步,你需要创建一个注解 `Region`。** ```java @Target({FIELD}) @@ -369,13 +373,9 @@ public @interface Region { } ``` -第二步你需要实现 `ConstraintValidator`接口,并重写`isValid` 方法: +**第二步,你需要实现 `ConstraintValidator`接口,并重写`isValid` 方法。** ```java -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import java.util.HashSet; - public class RegionValidator implements ConstraintValidator { @Override @@ -393,10 +393,25 @@ public class RegionValidator implements ConstraintValidator { 现在你就可以使用这个注解: ```java - @Region - private String region; +@Region +private String region; ``` +**通过测试验证** + +```java +PersonRequest personRequest = PersonRequest.builder() + .region("Shanghai").build(); +mockMvc.perform(post("/api/person") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(personRequest))) + .andExpect(MockMvcResultMatchers.jsonPath("region").value("Region 值不在可选范围内")); +``` + +**使用 `Postman` 验证** + +![](https://img-blog.csdnimg.cn/20210421203330978.png) + ### 案例二:校验电话号码 校验我们的电话号码是否合法,这个可以通过正则表达式来做,相关的正则表达式都可以在网上搜到,你甚至可以搜索到针对特定运营商电话号码段的正则表达式。 @@ -404,15 +419,6 @@ public class RegionValidator implements ConstraintValidator { `PhoneNumber.java` ```java -import javax.validation.Constraint; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - @Documented @Constraint(validatedBy = PhoneNumberValidator.class) @Target({FIELD, PARAMETER}) @@ -427,10 +433,7 @@ public @interface PhoneNumber { `PhoneNumberValidator.java` ```java -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -public class PhoneNumberValidator implements ConstraintValidator { +public class PhoneNumberValidator implements ConstraintValidator { @Override public boolean isValid(String phoneField, ConstraintValidatorContext context) { @@ -438,9 +441,15 @@ public class PhoneNumberValidator implements ConstraintValidator 8 && phoneField.length() < 14; + // 大陆手机号码11位数,匹配格式:前三位固定格式+后8位任意数 + // ^ 匹配输入字符串开始的位置 + // \d 匹配一个或多个数字,其中 \ 要转义,所以是 \\d + // $ 匹配输入字符串结尾的位置 + String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$"; + return phoneField.matches(regExp); } } + ``` 搞定,我们现在就可以使用这个注解了。 @@ -451,11 +460,28 @@ public class PhoneNumberValidator implements ConstraintValidator org.springframework.boot spring-boot-starter-parent - 2.1.8.RELEASE + 2.4.5 com.example @@ -19,22 +19,25 @@ - org.springframework.boot spring-boot-starter-web org.springframework.boot - spring-boot-devtools - runtime - true + spring-boot-starter-validation org.projectlombok lombok true + + junit + junit + 4.13.1 + test + org.springframework.boot spring-boot-starter-test diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java index 1b22d08..c0482b5 100644 --- a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java @@ -1,12 +1,9 @@ package com.example.beanvalidationdemo.controller; -import com.example.beanvalidationdemo.entity.Person; +import com.example.beanvalidationdemo.entity.PersonRequest; import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; -import org.springframework.validation.Errors; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.Mapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -20,21 +17,21 @@ import javax.validation.constraints.Size; @RestController -@RequestMapping("/api") +@RequestMapping("/api/persons") @Validated public class PersonController { - @PostMapping("/person") - public ResponseEntity getPerson(@RequestBody @Valid Person person) { - return ResponseEntity.ok().body(person); + @PostMapping + public ResponseEntity save(@RequestBody @Valid PersonRequest personRequest) { + return ResponseEntity.ok().body(personRequest); } - @GetMapping("/person/{id}") + @GetMapping("/{id}") public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) { return ResponseEntity.ok().body(id); } - @PutMapping("/person") + @PutMapping public ResponseEntity getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) { return ResponseEntity.ok().body(name); } diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java index d095320..b1b6dec 100644 --- a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java @@ -1,48 +1,21 @@ package com.example.beanvalidationdemo.entity; -import com.example.beanvalidationdemo.constants.Constants; import com.example.beanvalidationdemo.service.AddPersonGroup; import com.example.beanvalidationdemo.service.DeletePersonGroup; -import com.example.beanvalidationdemo.validation.PhoneNumber; -import com.example.beanvalidationdemo.validation.Region; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import javax.validation.constraints.Email; import javax.validation.constraints.NotNull; import javax.validation.constraints.Null; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; @Data -@AllArgsConstructor -@NoArgsConstructor public class Person { - @NotNull(message = "classId 不能为空") - private String classId; - - @Size(max = 33) - @NotNull(message = "name 不能为空") - private String name; - - @Pattern(regexp = Constants.sexs, message = "sex 值不在可选范围") - @NotNull(message = "sex 不能为空") - private String sex; - - @Email(message = "email 格式不正确") - @NotNull(message = "email 不能为空") - private String email; - - @PhoneNumber(message = "phoneNumber 格式不正确") - @NotNull(message = "phoneNumber 不能为空") - private String phoneNumber; - - @Region - private String region; - + // 当验证组为 DeletePersonGroup 的时候 group 字段不能为空 @NotNull(groups = DeletePersonGroup.class) + // 当验证组为 AddPersonGroup 的时候 group 字段需要为空 @Null(groups = AddPersonGroup.class) private String group; } diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java new file mode 100644 index 0000000..9b2c789 --- /dev/null +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java @@ -0,0 +1,44 @@ +package com.example.beanvalidationdemo.entity; + +import com.example.beanvalidationdemo.validation.PhoneNumber; +import com.example.beanvalidationdemo.validation.Region; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/04/21 20:48 + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PersonRequest { + + @NotNull(message = "classId 不能为空") + private String classId; + + @Size(max = 33) + @NotNull(message = "name 不能为空") + private String name; + + @Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围") + @NotNull(message = "sex 不能为空") + private String sex; + + @Region + private String region; + + @PhoneNumber(message = "phoneNumber 格式不正确") + @NotNull(message = "phoneNumber 不能为空") + private String phoneNumber; + +} diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java index 99856bf..08e8e14 100644 --- a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java @@ -1,6 +1,7 @@ package com.example.beanvalidationdemo.service; import com.example.beanvalidationdemo.entity.Person; +import com.example.beanvalidationdemo.entity.PersonRequest; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -10,7 +11,7 @@ @Validated public class PersonService { - public void validatePerson(@Valid Person person) { + public void validatePersonRequest(@Valid PersonRequest personRequest) { // do something } diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java index 8226e99..afd5373 100644 --- a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java @@ -15,6 +15,8 @@ @Retention(RUNTIME) public @interface PhoneNumber { String message() default "Invalid phone number"; + Class[] groups() default {}; + Class[] payload() default {}; } diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java index 4fc63c9..85acdcd 100644 --- a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java @@ -3,7 +3,7 @@ import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; -public class PhoneNumberValidator implements ConstraintValidator { +public class PhoneNumberValidator implements ConstraintValidator { @Override public boolean isValid(String phoneField, ConstraintValidatorContext context) { @@ -11,6 +11,11 @@ public boolean isValid(String phoneField, ConstraintValidatorContext context) { // can be null return true; } - return phoneField.matches("^1(3[0-9]|4[57]|5[0-35-9]|8[0-9]|70)\\d{8}$") && phoneField.length() > 8 && phoneField.length() < 14; + // 大陆手机号码11位数,匹配格式:前三位固定格式+后8位任意数 + // ^ 匹配输入字符串开始的位置 + // \d 匹配一个或多个数字,其中 \ 要转义,所以是 \\d + // $ 匹配输入字符串结尾的位置 + String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$"; + return phoneField.matches(regExp); } } diff --git a/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/HelloWorldControllerTest.java b/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/HelloWorldControllerTest.java deleted file mode 100644 index ea998ca..0000000 --- a/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/HelloWorldControllerTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.beanvalidationdemo; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; - -/** - * 验证基本的项目基本环境是否有问题 - */ -@RunWith(SpringRunner.class) -@SpringBootTest -@AutoConfigureMockMvc -public class HelloWorldControllerTest { - @Autowired - private MockMvc mockMvc; - - @Test - public void should_get_hello() throws Exception { - mockMvc.perform(get("/api/hello")).andExpect(content().string("Hello")); - } -} diff --git a/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java b/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java index 0ff4eb4..f020f05 100644 --- a/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java +++ b/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java @@ -1,15 +1,13 @@ package com.example.beanvalidationdemo; -import com.example.beanvalidationdemo.entity.Person; +import com.example.beanvalidationdemo.entity.PersonRequest; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @@ -25,7 +23,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class PersonControllerTest { @@ -35,85 +32,52 @@ public class PersonControllerTest { @Autowired private ObjectMapper objectMapper; - @Test - public void should_get_person() throws Exception { - Person person = new Person(); - person.setName("SnailClimb"); - person.setSex("Man"); - person.setClassId("82938390"); - person.setEmail("Snailclimb@qq.com"); - person.setRegion("China"); - person.setPhoneNumber("13615833391"); - - - mockMvc.perform(post("/api/person") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(person))) - .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb")) - .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390")) - .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man")) - .andExpect(MockMvcResultMatchers.jsonPath("email").value("Snailclimb@qq.com")); - ; - } + /** + * 验证出现参数不合法的情况抛出异常并且可以正确被捕获 + */ @Test public void should_check_person_value() throws Exception { - Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); - person.setRegion("BeiGuo"); - person.setPhoneNumber("1361583339"); - + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390") + .region("Shanghai") + .phoneNumber("1816313815").build(); mockMvc.perform(post("/api/person") - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(objectMapper.writeValueAsString(person))) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(personRequest))) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围")) .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空")) - .andExpect(MockMvcResultMatchers.jsonPath("email").value("email 格式不正确")) .andExpect(MockMvcResultMatchers.jsonPath("region").value("Region 值不在可选范围内")) .andExpect(MockMvcResultMatchers.jsonPath("phoneNumber").value("phoneNumber 格式不正确")); - } @Test - public void should_check_param_value() throws Exception { - + public void should_check_path_variable() throws Exception { mockMvc.perform(get("/api/person/6") - .contentType(MediaType.APPLICATION_JSON_UTF8)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) .andExpect(content().string("getPersonByID.id: 超过 id 的范围了")); } @Test - public void should_check_param_value2() throws Exception { - + public void should_check_request_param_value2() throws Exception { mockMvc.perform(put("/api/person") .param("name", "snailclimbsnailclimb") - .contentType(MediaType.APPLICATION_JSON_UTF8)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) .andExpect(content().string("getPersonByName.name: 超过 name 的范围了")); } /** - * 手动校验对象,很多场景下需要使用这种方式 + * 手动校验对象 */ @Test public void check_person_manually() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); - Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); - Set> violations = validator.validate(person); - //output: - //email 格式不正确 - //name 不能为空 - //sex 值不在可选范围 - for (ConstraintViolation constraintViolation : violations) { - System.out.println(constraintViolation.getMessage()); - } + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + Set> violations = validator.validate(personRequest); + violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); } } diff --git a/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java b/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java index 377d7e1..88b5da3 100644 --- a/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java +++ b/source-code/advanced/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java @@ -1,6 +1,7 @@ package com.example.beanvalidationdemo; import com.example.beanvalidationdemo.entity.Person; +import com.example.beanvalidationdemo.entity.PersonRequest; import com.example.beanvalidationdemo.service.PersonService; import org.junit.Test; import org.junit.runner.RunWith; @@ -16,21 +17,20 @@ public class PersonServiceTest { @Autowired private PersonService service; - @Test(expected = ConstraintViolationException.class) - public void should_throw_exception_when_person_is_not_valid() { - Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); - service.validatePerson(person); + @Test + public void should_throw_exception_when_person_request_is_not_valid() { + try { + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + service.validatePersonRequest(personRequest); + } catch (ConstraintViolationException e) { + e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); + } } @Test(expected = ConstraintViolationException.class) public void should_check_person_with_groups() { Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); person.setGroup("group1"); service.validatePersonGroupForAdd(person); } @@ -38,9 +38,6 @@ public void should_check_person_with_groups() { @Test(expected = ConstraintViolationException.class) public void should_check_person_with_groups2() { Person person = new Person(); - person.setSex("Man22"); - person.setClassId("82938390"); - person.setEmail("SnailClimb"); service.validatePersonGroupForDelete(person); } From 25f893f6851fa8f07db5b24af5d6c4656ea8d5a2 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 21:47:15 +0800 Subject: [PATCH 190/207] =?UTF-8?q?[feat]SpringBoot=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/beanvalidationdemo/entity/Person.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java index b1b6dec..80bb5e8 100644 --- a/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java +++ b/source-code/advanced/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java @@ -2,10 +2,7 @@ import com.example.beanvalidationdemo.service.AddPersonGroup; import com.example.beanvalidationdemo.service.DeletePersonGroup; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; -import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Null; From 47e3be859c7a76a07a079f973f6fef528b00da35 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 21:49:35 +0800 Subject: [PATCH 191/207] =?UTF-8?q?[feat]=E4=BB=A3=E7=A0=81=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E7=A7=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/.DS_Store | Bin 6148 -> 0 bytes .DS_Store => source-code/.DS_Store | Bin 6148 -> 6148 bytes .../.gitignore | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 114 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 source-code/bean-validation-demo/mvnw | 298 ++++++++++++++++++ .../mvnw.cmd | 0 source-code/bean-validation-demo/pom.xml | 57 ++++ .../BeanValidationDemoApplication.java | 13 + .../constants/Constants.java | 5 + .../controller/HelloWorldController.java | 19 ++ .../controller/PersonController.java | 38 +++ .../beanvalidationdemo/entity/Person.java | 18 ++ .../entity/PersonRequest.java | 44 +++ .../exception/GlobalExceptionHandler.java | 33 ++ .../service/AddPersonGroup.java | 4 + .../service/DeletePersonGroup.java | 4 + .../service/PersonService.java | 28 ++ .../validation/PhoneNumber.java | 22 ++ .../validation/PhoneNumberValidator.java | 21 ++ .../beanvalidationdemo/validation/Region.java | 26 ++ .../validation/RegionValidator.java | 17 + .../src/main/resources/application.properties | 0 .../PersonControllerTest.java | 83 +++++ .../beanvalidationdemo/PersonServiceTest.java | 44 +++ source-code/hello-world/.gitignore | 31 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + source-code/{start => }/hello-world/mvnw | 0 source-code/hello-world/mvnw.cmd | 161 ++++++++++ source-code/{start => }/hello-world/pom.xml | 0 .../helloworld/HelloWorldApplication.java | 0 .../helloworld/controller/UserController.java | 0 .../com/example/helloworld/dto/UserDto.java | 0 .../com/example/helloworld/entity/User.java | 0 .../src/main/resources/application.properties | 0 .../com/example/dto2entity/BeanUtilsTest.java | 0 .../HelloWorldApplicationTests.java | 0 40 files changed, 1081 insertions(+) delete mode 100644 .github/.DS_Store rename .DS_Store => source-code/.DS_Store (77%) rename source-code/{start/hello-world => bean-validation-demo}/.gitignore (100%) create mode 100644 source-code/bean-validation-demo/.mvn/wrapper/MavenWrapperDownloader.java rename source-code/{start/hello-world => bean-validation-demo}/.mvn/wrapper/maven-wrapper.jar (100%) rename source-code/{start/hello-world => bean-validation-demo}/.mvn/wrapper/maven-wrapper.properties (100%) create mode 100755 source-code/bean-validation-demo/mvnw rename source-code/{start/hello-world => bean-validation-demo}/mvnw.cmd (100%) create mode 100644 source-code/bean-validation-demo/pom.xml create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/BeanValidationDemoApplication.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/constants/Constants.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/HelloWorldController.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/exception/GlobalExceptionHandler.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/AddPersonGroup.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/DeletePersonGroup.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/Region.java create mode 100644 source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/RegionValidator.java create mode 100644 source-code/bean-validation-demo/src/main/resources/application.properties create mode 100644 source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java create mode 100644 source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java create mode 100644 source-code/hello-world/.gitignore rename source-code/{start => }/hello-world/.mvn/wrapper/MavenWrapperDownloader.java (100%) create mode 100644 source-code/hello-world/.mvn/wrapper/maven-wrapper.jar create mode 100644 source-code/hello-world/.mvn/wrapper/maven-wrapper.properties rename source-code/{start => }/hello-world/mvnw (100%) create mode 100644 source-code/hello-world/mvnw.cmd rename source-code/{start => }/hello-world/pom.xml (100%) rename source-code/{start => }/hello-world/src/main/java/com/example/helloworld/HelloWorldApplication.java (100%) rename source-code/{start => }/hello-world/src/main/java/com/example/helloworld/controller/UserController.java (100%) rename source-code/{start => }/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java (100%) rename source-code/{start => }/hello-world/src/main/java/com/example/helloworld/entity/User.java (100%) rename source-code/{start => }/hello-world/src/main/resources/application.properties (100%) rename source-code/{start => }/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java (100%) rename source-code/{start => }/hello-world/src/test/java/com/example/helloworld/HelloWorldApplicationTests.java (100%) diff --git a/.github/.DS_Store b/.github/.DS_Store deleted file mode 100644 index c29fa0aac579c0be056cac352053dbf583864b9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&5qMB5FWQ>*Xasv4@l+c3%4FpK$JbK6lKeS+e$%j0MsUGmqvtm)ubD--J(1O zywSb^ufvS(R#Yt)4hU5<()b(Ck1c=*zG3Da-F3`>H9xLquF|JbS!0CwjZ`X zMl)SSWn3lisM)T`tdF_SCcI{;H>zgMr^uFZ`#gl_$GT`sc65U%uWh?^fTwTY3e4 ziAGKxuHgq7dsccA&GSs>pD?H>LP%m6d+*9_44pil_| zhqXm>bYMfjj}&halAuj*2}0#Ca9CT!2#T`bP$0>ZkYjQV4H!a?RMz?fA#10|8^1gm;q+spcoLXVKfYJN$zglSsdN9 u3U!Q1LV2~tO$i$2D#l#8iXWpILBApcF>qL0L=Ou82pAf;VFvyw1HS;AXj9|> diff --git a/.DS_Store b/source-code/.DS_Store similarity index 77% rename from .DS_Store rename to source-code/.DS_Store index e848d82458ef7b90e87b4b72e9c66496134514de..7ad59510222be3cc1d27a91e171274be0e7e536c 100644 GIT binary patch delta 353 zcmZoMXfc=|#>B!ku~2NHo}wrV0|Nsi1A_nqLmopiLn=c`Qh9N~W4u(XA z6oxV&i6otrlb-~XJ2_d4si9t?y4uvzOh>`kz^qnBq1wX2LPx>O(zLdglS5q9(AF~{ zx3a3brnYV-PzMk&0xbgrekcv2W&s%(wv`1J<>ln(r32+a`dAr~fG#O!$Ydx6ITPd# ykR!nkNv5VlkSs!W0x(>@GHzz);O77aAdvH&c{0Cp6F!wdjB?M6HR delta 182 zcmZoMXfc=|#>B)qu~2NHo}wrd0|Nsi1A_nqLk2@BLsC+CaY0hf#=_-{lMO^zq}dtt z7}6Os8A_04^OJyjPE6KkZO{;}uC}z$Q7|?!sMS%ZHa9oWQ7|(ws;%YZ5LY#{^-RdE ztg5c5t(!6V2CKaAOrSP212?m=F*0pzU}E0P&cV+Cbk$}-j_=Hq`9&N#fQms5W7!-b HvW6J|&kZW) diff --git a/source-code/start/hello-world/.gitignore b/source-code/bean-validation-demo/.gitignore similarity index 100% rename from source-code/start/hello-world/.gitignore rename to source-code/bean-validation-demo/.gitignore diff --git a/source-code/bean-validation-demo/.mvn/wrapper/MavenWrapperDownloader.java b/source-code/bean-validation-demo/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..7f91a56 --- /dev/null +++ b/source-code/bean-validation-demo/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,114 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/source-code/start/hello-world/.mvn/wrapper/maven-wrapper.jar b/source-code/bean-validation-demo/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from source-code/start/hello-world/.mvn/wrapper/maven-wrapper.jar rename to source-code/bean-validation-demo/.mvn/wrapper/maven-wrapper.jar diff --git a/source-code/start/hello-world/.mvn/wrapper/maven-wrapper.properties b/source-code/bean-validation-demo/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from source-code/start/hello-world/.mvn/wrapper/maven-wrapper.properties rename to source-code/bean-validation-demo/.mvn/wrapper/maven-wrapper.properties diff --git a/source-code/bean-validation-demo/mvnw b/source-code/bean-validation-demo/mvnw new file mode 100755 index 0000000..c188654 --- /dev/null +++ b/source-code/bean-validation-demo/mvnw @@ -0,0 +1,298 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="$(/usr/libexec/java_home)" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +if [ -z "$M2_HOME" ]; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then + PRG="$link" + else + PRG="$(dirname "$PRG")/$link" + fi + done + + saveddir=$(pwd) + + M2_HOME=$(dirname "$PRG")/.. + + # make it fully qualified + M2_HOME=$(cd "$M2_HOME" && pwd) + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=$(cygpath --unix "$M2_HOME") + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$M2_HOME" ] && + M2_HOME="$( ( + cd "$M2_HOME" + pwd + ))" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="$( ( + cd "$JAVA_HOME" + pwd + ))" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr \"$javaExecutable\" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! $(expr "$readLink" : '\([^ ]*\)') = "no" ]; then + if $darwin; then + javaHome="$(dirname \"$javaExecutable\")" + javaExecutable="$(cd \"$javaHome\" && pwd -P)/javac" + else + javaExecutable="$(readlink -f \"$javaExecutable\")" + fi + javaHome="$(dirname \"$javaExecutable\")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(which java)" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." + pwd + ) + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' <"$1")" + fi +} + +BASE_DIR=$(find_maven_basedir "$(pwd)") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in wrapperUrl) + jarUrl="$value" + break + ;; + esac + done <"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget >/dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl >/dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=$(cygpath --path --windows "$M2_HOME") + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/source-code/start/hello-world/mvnw.cmd b/source-code/bean-validation-demo/mvnw.cmd similarity index 100% rename from source-code/start/hello-world/mvnw.cmd rename to source-code/bean-validation-demo/mvnw.cmd diff --git a/source-code/bean-validation-demo/pom.xml b/source-code/bean-validation-demo/pom.xml new file mode 100644 index 0000000..c5a9e2f --- /dev/null +++ b/source-code/bean-validation-demo/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.5 + + + com.example + bean-validation-demo + 0.0.1-SNAPSHOT + bean-validation-demo + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + true + + + junit + junit + 4.13.1 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/BeanValidationDemoApplication.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/BeanValidationDemoApplication.java new file mode 100644 index 0000000..3f934bc --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/BeanValidationDemoApplication.java @@ -0,0 +1,13 @@ +package com.example.beanvalidationdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BeanValidationDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(BeanValidationDemoApplication.class, args); + } + +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/constants/Constants.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/constants/Constants.java new file mode 100644 index 0000000..8c20af3 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/constants/Constants.java @@ -0,0 +1,5 @@ +package com.example.beanvalidationdemo.constants; + +public final class Constants { + public static final String sexs = "((^Man$|^Woman$|^UGM$))"; +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/HelloWorldController.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/HelloWorldController.java new file mode 100644 index 0000000..8742cbf --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/HelloWorldController.java @@ -0,0 +1,19 @@ +package com.example.beanvalidationdemo.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author shuang.kou + * 验证基本环境搭建是否正确 + */ +@RestController +@RequestMapping("/api") +public class HelloWorldController { + + @GetMapping("/hello") + public String hello() { + return "Hello"; + } +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java new file mode 100644 index 0000000..c0482b5 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/controller/PersonController.java @@ -0,0 +1,38 @@ +package com.example.beanvalidationdemo.controller; + +import com.example.beanvalidationdemo.entity.PersonRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Size; + +@RestController +@RequestMapping("/api/persons") +@Validated +public class PersonController { + + @PostMapping + public ResponseEntity save(@RequestBody @Valid PersonRequest personRequest) { + return ResponseEntity.ok().body(personRequest); + } + + @GetMapping("/{id}") + public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) { + return ResponseEntity.ok().body(id); + } + + @PutMapping + public ResponseEntity getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) { + return ResponseEntity.ok().body(name); + } +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java new file mode 100644 index 0000000..80bb5e8 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/Person.java @@ -0,0 +1,18 @@ +package com.example.beanvalidationdemo.entity; + +import com.example.beanvalidationdemo.service.AddPersonGroup; +import com.example.beanvalidationdemo.service.DeletePersonGroup; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; + +@Data +public class Person { + + // 当验证组为 DeletePersonGroup 的时候 group 字段不能为空 + @NotNull(groups = DeletePersonGroup.class) + // 当验证组为 AddPersonGroup 的时候 group 字段需要为空 + @Null(groups = AddPersonGroup.class) + private String group; +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java new file mode 100644 index 0000000..9b2c789 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/entity/PersonRequest.java @@ -0,0 +1,44 @@ +package com.example.beanvalidationdemo.entity; + +import com.example.beanvalidationdemo.validation.PhoneNumber; +import com.example.beanvalidationdemo.validation.Region; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/04/21 20:48 + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PersonRequest { + + @NotNull(message = "classId 不能为空") + private String classId; + + @Size(max = 33) + @NotNull(message = "name 不能为空") + private String name; + + @Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围") + @NotNull(message = "sex 不能为空") + private String sex; + + @Region + private String region; + + @PhoneNumber(message = "phoneNumber 格式不正确") + @NotNull(message = "phoneNumber 不能为空") + private String phoneNumber; + +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/exception/GlobalExceptionHandler.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..c4f4e0a --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/exception/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package com.example.beanvalidationdemo.exception; + +import com.example.beanvalidationdemo.controller.PersonController; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import javax.validation.ConstraintViolationException; +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice(assignableTypes = {PersonController.class}) +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions( + MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors); + } + + @ExceptionHandler(ConstraintViolationException.class) + ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/AddPersonGroup.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/AddPersonGroup.java new file mode 100644 index 0000000..1f2950d --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/AddPersonGroup.java @@ -0,0 +1,4 @@ +package com.example.beanvalidationdemo.service; + +public interface AddPersonGroup { +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/DeletePersonGroup.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/DeletePersonGroup.java new file mode 100644 index 0000000..92f6713 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/DeletePersonGroup.java @@ -0,0 +1,4 @@ +package com.example.beanvalidationdemo.service; + +public interface DeletePersonGroup { +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java new file mode 100644 index 0000000..08e8e14 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/service/PersonService.java @@ -0,0 +1,28 @@ +package com.example.beanvalidationdemo.service; + +import com.example.beanvalidationdemo.entity.Person; +import com.example.beanvalidationdemo.entity.PersonRequest; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; + +@Service +@Validated +public class PersonService { + + public void validatePersonRequest(@Valid PersonRequest personRequest) { + // do something + } + + @Validated(AddPersonGroup.class) + public void validatePersonGroupForAdd(@Valid Person person) { + // do something + } + + @Validated(DeletePersonGroup.class) + public void validatePersonGroupForDelete(@Valid Person person) { + // do something + } + +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java new file mode 100644 index 0000000..afd5373 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumber.java @@ -0,0 +1,22 @@ +package com.example.beanvalidationdemo.validation; + +import javax.validation.Constraint; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Constraint(validatedBy = PhoneNumberValidator.class) +@Target({FIELD, PARAMETER}) +@Retention(RUNTIME) +public @interface PhoneNumber { + String message() default "Invalid phone number"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java new file mode 100644 index 0000000..85acdcd --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/PhoneNumberValidator.java @@ -0,0 +1,21 @@ +package com.example.beanvalidationdemo.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class PhoneNumberValidator implements ConstraintValidator { + + @Override + public boolean isValid(String phoneField, ConstraintValidatorContext context) { + if (phoneField == null) { + // can be null + return true; + } + // 大陆手机号码11位数,匹配格式:前三位固定格式+后8位任意数 + // ^ 匹配输入字符串开始的位置 + // \d 匹配一个或多个数字,其中 \ 要转义,所以是 \\d + // $ 匹配输入字符串结尾的位置 + String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$"; + return phoneField.matches(regExp); + } +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/Region.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/Region.java new file mode 100644 index 0000000..8b0a204 --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/Region.java @@ -0,0 +1,26 @@ +package com.example.beanvalidationdemo.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author shuang.kou + */ +@Target({FIELD}) +@Retention(RUNTIME) +@Constraint(validatedBy = RegionValidator.class) +@Documented +public @interface Region { + + String message() default "Region 值不在可选范围内"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/RegionValidator.java b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/RegionValidator.java new file mode 100644 index 0000000..9490c5b --- /dev/null +++ b/source-code/bean-validation-demo/src/main/java/com/example/beanvalidationdemo/validation/RegionValidator.java @@ -0,0 +1,17 @@ +package com.example.beanvalidationdemo.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.HashSet; + +public class RegionValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + HashSet regions = new HashSet<>(); + regions.add("China"); + regions.add("China-Taiwan"); + regions.add("China-HongKong"); + return regions.contains(value); + } +} diff --git a/source-code/bean-validation-demo/src/main/resources/application.properties b/source-code/bean-validation-demo/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java b/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java new file mode 100644 index 0000000..f020f05 --- /dev/null +++ b/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java @@ -0,0 +1,83 @@ +package com.example.beanvalidationdemo; + + +import com.example.beanvalidationdemo.entity.PersonRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class PersonControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + + /** + * 验证出现参数不合法的情况抛出异常并且可以正确被捕获 + */ + @Test + public void should_check_person_value() throws Exception { + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390") + .region("Shanghai") + .phoneNumber("1816313815").build(); + mockMvc.perform(post("/api/person") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(personRequest))) + .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围")) + .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空")) + .andExpect(MockMvcResultMatchers.jsonPath("region").value("Region 值不在可选范围内")) + .andExpect(MockMvcResultMatchers.jsonPath("phoneNumber").value("phoneNumber 格式不正确")); + } + + @Test + public void should_check_path_variable() throws Exception { + mockMvc.perform(get("/api/person/6") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("getPersonByID.id: 超过 id 的范围了")); + } + + @Test + public void should_check_request_param_value2() throws Exception { + mockMvc.perform(put("/api/person") + .param("name", "snailclimbsnailclimb") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("getPersonByName.name: 超过 name 的范围了")); + } + + /** + * 手动校验对象 + */ + @Test + public void check_person_manually() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + Set> violations = validator.validate(personRequest); + violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); + } +} diff --git a/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java b/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java new file mode 100644 index 0000000..88b5da3 --- /dev/null +++ b/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonServiceTest.java @@ -0,0 +1,44 @@ +package com.example.beanvalidationdemo; + +import com.example.beanvalidationdemo.entity.Person; +import com.example.beanvalidationdemo.entity.PersonRequest; +import com.example.beanvalidationdemo.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 javax.validation.ConstraintViolationException; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class PersonServiceTest { + @Autowired + private PersonService service; + + @Test + public void should_throw_exception_when_person_request_is_not_valid() { + try { + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + service.validatePersonRequest(personRequest); + } catch (ConstraintViolationException e) { + e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); + } + } + + @Test(expected = ConstraintViolationException.class) + public void should_check_person_with_groups() { + Person person = new Person(); + person.setGroup("group1"); + service.validatePersonGroupForAdd(person); + } + + @Test(expected = ConstraintViolationException.class) + public void should_check_person_with_groups2() { + Person person = new Person(); + service.validatePersonGroupForDelete(person); + } + +} diff --git a/source-code/hello-world/.gitignore b/source-code/hello-world/.gitignore new file mode 100644 index 0000000..a2a3040 --- /dev/null +++ b/source-code/hello-world/.gitignore @@ -0,0 +1,31 @@ +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/ diff --git a/source-code/start/hello-world/.mvn/wrapper/MavenWrapperDownloader.java b/source-code/hello-world/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from source-code/start/hello-world/.mvn/wrapper/MavenWrapperDownloader.java rename to source-code/hello-world/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/source-code/hello-world/.mvn/wrapper/maven-wrapper.jar b/source-code/hello-world/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/source-code/start/hello-world/pom.xml b/source-code/hello-world/pom.xml similarity index 100% rename from source-code/start/hello-world/pom.xml rename to source-code/hello-world/pom.xml diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/HelloWorldApplication.java b/source-code/hello-world/src/main/java/com/example/helloworld/HelloWorldApplication.java similarity index 100% rename from source-code/start/hello-world/src/main/java/com/example/helloworld/HelloWorldApplication.java rename to source-code/hello-world/src/main/java/com/example/helloworld/HelloWorldApplication.java diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/controller/UserController.java b/source-code/hello-world/src/main/java/com/example/helloworld/controller/UserController.java similarity index 100% rename from source-code/start/hello-world/src/main/java/com/example/helloworld/controller/UserController.java rename to source-code/hello-world/src/main/java/com/example/helloworld/controller/UserController.java diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java b/source-code/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java similarity index 100% rename from source-code/start/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java rename to source-code/hello-world/src/main/java/com/example/helloworld/dto/UserDto.java diff --git a/source-code/start/hello-world/src/main/java/com/example/helloworld/entity/User.java b/source-code/hello-world/src/main/java/com/example/helloworld/entity/User.java similarity index 100% rename from source-code/start/hello-world/src/main/java/com/example/helloworld/entity/User.java rename to source-code/hello-world/src/main/java/com/example/helloworld/entity/User.java diff --git a/source-code/start/hello-world/src/main/resources/application.properties b/source-code/hello-world/src/main/resources/application.properties similarity index 100% rename from source-code/start/hello-world/src/main/resources/application.properties rename to source-code/hello-world/src/main/resources/application.properties diff --git a/source-code/start/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java b/source-code/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java similarity index 100% rename from source-code/start/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java rename to source-code/hello-world/src/test/java/com/example/dto2entity/BeanUtilsTest.java diff --git a/source-code/start/hello-world/src/test/java/com/example/helloworld/HelloWorldApplicationTests.java b/source-code/hello-world/src/test/java/com/example/helloworld/HelloWorldApplicationTests.java similarity index 100% rename from source-code/start/hello-world/src/test/java/com/example/helloworld/HelloWorldApplicationTests.java rename to source-code/hello-world/src/test/java/com/example/helloworld/HelloWorldApplicationTests.java From 2355126d2186923f7a1eb7d98ff2f3e927883e1f Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 21:55:33 +0800 Subject: [PATCH 192/207] Delete workspace.xml --- springboot-schedule-task/.idea/workspace.xml | 118 ------------------- 1 file changed, 118 deletions(-) delete mode 100644 springboot-schedule-task/.idea/workspace.xml diff --git a/springboot-schedule-task/.idea/workspace.xml b/springboot-schedule-task/.idea/workspace.xml deleted file mode 100644 index 7120d02..0000000 --- a/springboot-schedule-task/.idea/workspace.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1573625912874 - - - - - - - - - \ No newline at end of file From c1a4017eafcde5adc99b354071ecaa950cfaa270 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 21:56:44 +0800 Subject: [PATCH 193/207] Delete .DS_Store --- source-code/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 source-code/.DS_Store diff --git a/source-code/.DS_Store b/source-code/.DS_Store deleted file mode 100644 index 7ad59510222be3cc1d27a91e171274be0e7e536c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}xR_5T5d*1!Lr3BFA35aex>I985Nd2X7{8^q>ZIT_T&{E@2mmB4%Gh-^eHM zb)0F70nx;hG0;h7zGkyimI002iN%;eF0BNQiHl9ZmwATqf}05{MD z&mBjDrPXG_Fkl$?Zw%1c<=}t^_h{$NuiFn@jJ>&oz8}Tq@;ghVGue$z&htFKoxiqw z;n-CbTHLBBTj_>VP4o^CF508_l>GO+Xmf=Mt zWk=%-UeQ@9^xhr@{V=%280BTbMG_+LA)+g66)Bi2@k{YDq71GJ1Nx}&t#v8b)M*$n z4E&k_x*r58p`|fVD6bAQvIRh--v}w_Q!ha|LZhWIQHT)~rcx1AD$^|nQ|UM_^g2sp zqEMv+)6ECdUuL>PVe;#^zmUs;SqhCc3>XF$8Az*Pf!_b)pZovCBr`G$7zS2~0hVdm zttOVF-`1t#=&co~9aIupmnf7eX!LU|9eRq3s8TQ{$U(F;CJJ!|#rz0J8jLXv{3!z; D4SSX| From f7a5770b131e90f72125822771a5a14dc1ba84b4 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 21:57:44 +0800 Subject: [PATCH 194/207] Update spring-bean-validation.md --- docs/advanced/spring-bean-validation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/advanced/spring-bean-validation.md b/docs/advanced/spring-bean-validation.md index e7a350d..dc6323d 100644 --- a/docs/advanced/spring-bean-validation.md +++ b/docs/advanced/spring-bean-validation.md @@ -29,7 +29,9 @@ public class PersonController { 不了解的朋友一定要好好看一下,学完马上就可以实践到项目上去。 -并且,本文实例项目使用的是目前最新的 Spring Boot 版本 2.4.5!(截止到 2021-04-21) +并且,本文示例项目使用的是目前最新的 Spring Boot 版本 2.4.5!(截止到 2021-04-21) + +示例项目源代码地址:[https://github.com/CodingDocs/springboot-guide/tree/master/source-code/bean-validation-demo](https://github.com/CodingDocs/springboot-guide/tree/master/source-code/bean-validation-demo) 。 ## 添加相关依赖 From df3a24817a7b89629868e6e4b51e8580804d4f1e Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 22:05:49 +0800 Subject: [PATCH 195/207] =?UTF-8?q?[feat]=E6=96=87=E4=BB=B6=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E7=A7=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/{advanced => }/spring-bean-validation.md | 0 docs/start/springboot-hello-world.md | 53 ++++++++++--------- 2 files changed, 27 insertions(+), 26 deletions(-) rename docs/{advanced => }/spring-bean-validation.md (100%) diff --git a/docs/advanced/spring-bean-validation.md b/docs/spring-bean-validation.md similarity index 100% rename from docs/advanced/spring-bean-validation.md rename to docs/spring-bean-validation.md diff --git a/docs/start/springboot-hello-world.md b/docs/start/springboot-hello-world.md index 5a8d16d..9d6d49a 100644 --- a/docs/start/springboot-hello-world.md +++ b/docs/start/springboot-hello-world.md @@ -1,22 +1,26 @@ -### 新建 Spring Boot 项目常用的两种方式 +示例项目源代码地址:[https://github.com/CodingDocs/springboot-guide/tree/master/source-code/hello-world](https://github.com/CodingDocs/springboot-guide/tree/master/source-code/hello-world) 。 -你可以通过 https://start.spring.io/ 这个网站来生成一个 Spring Boot 的项目。 +## 新建 Spring Boot 项目 -![start.spring.io](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/spring.start.io.png) +**1. 你可以通过 [https://start.spring.io/](https://start.spring.io/) 这个网站来生成一个 Spring Boot 的项目。** + +![start.spring.io](https://img-blog.csdnimg.cn/img_convert/69f88b648b5f94fe8fa509fc4d987057.png) 注意勾选上 Spring Web 这个模块,这是我们所必需的一个依赖。当所有选项都勾选完毕之后,点击下方的按钮 Generate 下载这个 Spring Boot 的项目。下载完成并解压之后,我们直接使用 IDEA 打开即可。 -当然你也可以直接通过 IDEA 来生成一个 Spring Boot 的项目,具体方法和上面类似:`File->New->Project->Spring Initializr`。 +另外,阿里也有一个类似的网站 [https://start.aliyun.com/bootstrap.html](https://start.aliyun.com/bootstrap.html) ,功能甚至还要更强大点,支持生成特定应用架构的项目模板! -### Spring Boot 项目结构分析 +![](https://img-blog.csdnimg.cn/20210421220259726.png) -成功打开项目之后,项目长下面这个样子: +**2.你也可以直接通过 IDEA 来生成一个 Spring Boot 的项目,具体方法和上面类似:`File->New->Project->Spring Initializr`。** - +## Spring Boot 项目结构分析 +成功打开项目之后,项目长下面这个样子: +![](https://img-blog.csdnimg.cn/img_convert/1f1a3ee40347e941891988cbb72ceee1.png#pic_center) -以 Application为后缀名的 Java 类一般就是 Spring Boot 的启动类,比如本项目的启动项目就是`HelloWorldApplication` 。我们直接像运行普通 Java 程序一样运行它,由于 Spring Boot 本身就嵌入servlet容器的缘故,我们的 web 项目就运行成功了, 非常方便。 +以 Application 为后缀名的 Java 类一般就是 Spring Boot 的启动类,比如本项目的启动项目就是`HelloWorldApplication` 。我们直接像运行普通 Java 程序一样运行它,由于 Spring Boot 本身就嵌入 servlet 容器的缘故,我们的 web 项目就运行成功了, 非常方便。 需要注意的一点是 **Spring Boot 的启动类是需要最外层的,不然可能导致一些类无法被正确扫描到,导致一些奇怪的问题。** 一般情况下 Spring Boot 项目结构类似下面这样 @@ -35,19 +39,19 @@ com | +- controller | +- CustomerController.java - | + | +- config | +- swagerConfig.java | ``` -1. ` Application.java`是项目的启动类 -2. domain目录主要用于实体(Entity)与数据访问层(Repository) +1. `Application.java`是项目的启动类 +2. domain 目录主要用于实体(Entity)与数据访问层(Repository) 3. service 层主要是业务类代码 4. controller 负责页面访问控制 5. config 目录主要放一些配置类 -### @SpringBootApplication 注解分析 +## @SpringBootApplication 注解分析 `HelloWorldApplication` @@ -91,21 +95,21 @@ public @interface SpringBootConfiguration { } ``` -可以看出大概可以把 `@SpringBootApplication `看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan ` 注解的集合。根据 SpringBoot官网,这三个注解的作用分别是: +可以看出大概可以把 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是: - `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 -- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的bean,注解默认会扫描该类所在的包下所有的类。 -- `@Configuration`:允许在上下文中注册额外的bean或导入其他配置类。 +- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 +- `@Configuration`:允许在上下文中注册额外的 bean 或导入其他配置类。 -所以说 `@SpringBootApplication `就是几个重要的注解的组合,为什么要有它?当然是为了省事,避免了我们每次开发 Spring Boot 项目都要写一些必备的注解。这一点在我们平时开发中也经常用到,比如我们通常会提一个测试基类,这个基类包含了我们写测试所需要的一些基本的注解和一些依赖。 +所以说 `@SpringBootApplication`就是几个重要的注解的组合,为什么要有它?当然是为了省事,避免了我们每次开发 Spring Boot 项目都要写一些必备的注解。这一点在我们平时开发中也经常用到,比如我们通常会提一个测试基类,这个基类包含了我们写测试所需要的一些基本的注解和一些依赖。 -### 新建一个 Controller +## 新建一个 Controller 上面说了这么多,我们现在正式开始写 Spring Boot 版的 “Hello World” 吧。 -新建一个 controller 文件夹,并在这个文件夹下新建一个名字叫做 `HelloWorldController` 的类。 +新建一个 `controller` 文件夹,并在这个文件夹下新建一个名字叫做 `HelloWorldController` 的类。 -`@RestController`是Spring 4 之后新加的注解,如果在Spring4之前开发 RESTful Web服务的话,你需要使用`@Controller` 并结合`@ResponseBody`注解,也就是说`@Controller` +`@ResponseBody`= `@RestController`。对于这两个注解,我在基础篇单独抽了一篇文章来介绍。 +`@RestController`是 Spring 4 之后新加的注解,如果在 Spring4 之前开发 RESTful Web 服务的话,你需要使用`@Controller` 并结合`@ResponseBody`注解,也就是说`@Controller` +`@ResponseBody`= `@RestController`。对于这两个注解,我在基础篇单独抽了一篇文章来介绍。 `com.example.helloworld.controller` @@ -134,9 +138,9 @@ public class HelloWorldController { server.port=8333 ``` -### 大功告成,运行项目 +## 大功告成,运行项目 -运行 `HelloWorldApplication` ,运行成功控制台会打印出一些消息,不要忽略这些消息,它里面会有一些比较有用的信息。 +运行 `HelloWorldApplication` ,运行成功控制台会打印出一些消息,不要忽略这些消息,它里面会有一些比较有用的信息。 ``` 2019-10-03 09:24:47.757 INFO 26326 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8333 (http) with context path '' @@ -146,9 +150,6 @@ server.port=8333 浏览器 http://localhost:8333/test/hello 如果你可以在页面正确得到 "Hello World" 的话,说明你已经成功完成了这部分内容。 -### 总结 - -通过本文我们学到了如何新建 Spring Boot 项目、SpringBoot 项目常见的项目结构分析、`@SpringBootApplication` 注解分析,最后实现了 Spring Boot 版的 "Hello World"。 - -代码地址: [https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world](https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world)(建议自己手敲一遍!!!) +## 总结 +通过本文我们学到了如何新建 Spring Boot 项目、SpringBoot 项目常见的项目结构分析、`@SpringBootApplication` 注解分析,最后实现了 Spring Boot 版的 "Hello World"。 \ No newline at end of file From 90536114964eaa324ac0fd07131854d0a7935cb9 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 22:06:18 +0800 Subject: [PATCH 196/207] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e13f958..f3f2212 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ### 进阶 1. Bean映射工具 :[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md) 、[5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md) -3. [如何在 Spring/Spring Boot 中做参数校验?](./docs/advanced/spring-bean-validation.md) +3. [如何在 Spring/Spring Boot 中做参数校验?](./docs/spring-bean-validation.md) 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) 5. [新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md) 7. [Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka) From 74188190bfbf0b6c8de4fb36bcd7dd0d801e66d8 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 22:29:03 +0800 Subject: [PATCH 197/207] =?UTF-8?q?[feat]=E4=BD=BF=E7=94=A8=20PowerMockRun?= =?UTF-8?q?ner=20=E5=92=8C=20Mockito=20=E7=BC=96=E5=86=99=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +- docs/PowerMockRunnerAndMockito.md | 161 +++++++++++++++++++++ docs/basis/PowerMockRunnerAndMockito.md | 177 ------------------------ 3 files changed, 166 insertions(+), 181 deletions(-) create mode 100644 docs/PowerMockRunnerAndMockito.md delete mode 100644 docs/basis/PowerMockRunnerAndMockito.md diff --git a/README.md b/README.md index f3f2212..73eeb7f 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,13 @@ ### 进阶 1. Bean映射工具 :[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md) 、[5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md) -3. [如何在 Spring/Spring Boot 中做参数校验?](./docs/spring-bean-validation.md) +3. [如何在 Spring/Spring Boot 中优雅地做参数校验?](./docs/spring-bean-validation.md) +3. [使用 PowerMockRunner 和 Mockito 编写单元测试用例](./docs/PowerMockRunnerAndMockito.md) 4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md) 5. [新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md) -7. [Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka) -8. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) -9. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) +6. [Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka) +7. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md) +8. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide) ### 补充 diff --git a/docs/PowerMockRunnerAndMockito.md b/docs/PowerMockRunnerAndMockito.md new file mode 100644 index 0000000..469e075 --- /dev/null +++ b/docs/PowerMockRunnerAndMockito.md @@ -0,0 +1,161 @@ +单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在 Spring 框架中常用的两种测试框架:`PowerMockRunner` 和 `SpringRunner` 两个单元测试,鉴于 `SpringRunner` 启动的一系列依赖和数据连接的问题,推荐使用 `PowerMockRunner`,这样能有效的提高测试的效率,并且其提供的 API 能覆盖的场景广泛,使用方便,可谓是 Java 单元测试之模拟利器。 + +## 1. PowerMock 是什么? + +`PowerMock` 是一个 Java 模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用 `PowerMock`,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。`PowerMock` 通过在执行测试时在运行时修改字节码来完成这些技巧。`PowerMock` 还包含一些实用程序,可让您更轻松地访问对象的内部状态。 + +举个例子,你在使用 `Junit` 进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用 `PowerMock`,拦截数据库操作,并模拟返回参数。 + +## 2. PowerMock 包引入 + +```xml + + + org.powermock + powermock-core + 2.0.2 + test + + + org.mockito + mockito-core + 2.23.0 + + + org.powermock + powermock-module-junit4 + 2.0.4 + test + + + org.powermock + powermock-api-mockito2 + 2.0.2 + test + + + com.github.jsonzou + jmockdata + 4.3.0 + +``` + +## 3. 重要注解说明 + +```java +@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试 +@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类 +@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误 +``` + +## 4. 使用示例 + +### 4.1 模拟接口返回 + +首先对接口进行 mock,然后录制相关行为 + +```java +InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class) +Powermockito.when(mock.method(Params…)).thenReturn(value) +Powermockito.when(mock.method(Params..)).thenThrow(Exception) +``` + +### 4.2 设置对象的 private 属性 + +需要使用 `Whitebox` 向 class 或者对象中赋值。 + +如我们已经对接口尽心了 mock,现在需要将此 mock 加入到对象中,可以采用如下方法: + +```java +Whitebox.setInternalState(Object object, String fieldname, Object… value); +``` + +其中 object 为需要设置属性的静态类或对象。 + +### 4.3 模拟构造函数 + +对于模拟构造函数,也即当出现 `new InstanceClass()` 时可以将此构造函数拦截并替换结果为我们需要的 mock 对象。 + +注意:使用时需要加入标记: + +```java +@RunWith(PowerMockRunner.class) +@PrepareForTest({ InstanceClass.class }) +@PowerMockIgnore("javax.management.\*") + +Powermockito.whenNew(InstanceClass.class).thenReturn(Object value) +``` + +##### 4.4 模拟静态方法 + +模拟静态方法类似于模拟构造函数,也需要加入注释标记。 + +```java +@RunWith(PowerMockRunner.class) +@PrepareForTest({ StaticClassToMock.class }) +@PowerMockIgnore("javax.management.\*") + +Powermockito.mockStatic(StaticClassToMock.class); +Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) +``` + +##### 4.5 模拟 final 方法 + +Final 方法的模拟类似于模拟静态方法。 + +```java +@RunWith(PowerMockRunner.class) +@PrepareForTest({ FinalClassToMock.class }) +@PowerMockIgnore("javax.management.\*") + +Powermockito.mockStatic(FinalClassToMock.class); +Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) +``` + +### 4.6 模拟静态类 + +模拟静态类类似于模拟静态方法。 + +### 4.7 使用 spy 方法避免执行被测类中的成员函数 + +如被测试类为:TargetClass,想要屏蔽的方法为 targetMethod. + +```java +1) PowerMockito.spy(TargetClass.class); + +2) Powemockito.when(TargetClass.targetMethod()).doReturn() + +3) 注意加入 + +@RunWith(PowerMockRunner.class) +@PrepareForTest(DisplayMoRelationBuilder.class) +@PowerMockIgnore("javax.management.*") +``` + +### 4.8 参数匹配器 + +有时我们在处理 `doMethod(Param param)` 时,不想进行精确匹配,这时可以使用 `Mockito` 提供的模糊匹配方式。 + +如:`Mockito.anyInt()`,`Mockito.anyString()` + +### 4.9 处理 public void 型的静态方法 + +```java +Powermockito.doNothing.when(T class2mock, String method, … params> +``` + +## 5. 单元测试用例可选清单 + +输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。 + +- 必传项测试 +- 唯一字段值测试 +- 空值测试 +- 字段只接受允许的字符 +- 负值测试 +- 字段限于字段长度规范 +- 不可能的值 +- 垃圾值测试 +- 检查字段之间的依赖性 +- 等效类划分和边界条件测试 +- 错误和异常处理测试单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在 Spring 框架中常用的两种测试框架:PowerMockRunner 和 SpringRunner 两个单元测试,鉴于 SpringRunner 启动的一系列依赖和数据连接的问题,推荐使用 PowerMockRunner,这样能有效的提高测试的效率,并且其提供的 API 能覆盖的场景广泛,使用方便,可谓是 Java 单元测试之模拟利器。 \ No newline at end of file diff --git a/docs/basis/PowerMockRunnerAndMockito.md b/docs/basis/PowerMockRunnerAndMockito.md deleted file mode 100644 index d5a7b12..0000000 --- a/docs/basis/PowerMockRunnerAndMockito.md +++ /dev/null @@ -1,177 +0,0 @@ -单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在Spring框架中常用的两种测试框架:PowerMockRunner和SpringRunner两个单元测试,鉴于SpringRunner启动的一系列依赖和数据连接的问题,推荐使用PowerMockRunner,这样能有效的提高测试的效率,并且其提供的API能覆盖的场景广泛,使用方便,可谓是Java单元测试之模拟利器。 - -#### 1. PowerMock是什么? - -PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用PowerMock,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可让您更轻松地访问对象的内部状态。 - - -举个例子,你在使用Junit进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用PowerMock,拦截数据库操作,并模拟返回参数。 - -#### 2. PowerMock包引入 - -```xml - - - org.powermock - powermock-core - 2.0.2 - test - - -org.mockito -mockito-core -2.23.0 - - -org.powermock -powermock-module-junit4 -2.0.4 -test - - -org.powermock -powermock-api-mockito2 -2.0.2 -test - - -com.github.jsonzou -jmockdata -4.3.0 - - -``` - -#### 3. 重要注解说明 - -```java -@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试 -@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类 -@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误 -``` - - - -#### 4. 使用示例 - -##### 4.1 模拟接口返回 - -首先对接口进行mock,然后录制相关行为 - -```java -InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class) - -Powermockito.when(mock.method(Params…)).thenReturn(value) - -Powermockito.when(mock.method(Params..)).thenThrow(Exception) -``` - -##### 4.2 设置对象的private属性 - -需要使用whitebox向class或者对象中赋值。 - -如我们已经对接口尽心了mock,现在需要将此mock加入到对象中,可以采用如下方法: - -```java -Whitebox.setInternalState(Object object, String fieldname, Object… value); -``` - -其中object为需要设置属性的静态类或对象。 - -##### 4.3 模拟构造函数 - -对于模拟构造函数,也即当出现new InstanceClass()时可以将此构造函数拦截并替换结果为我们需要的mock对象。 - -注意:使用时需要加入标记: - -```java -@RunWith(PowerMockRunner.class) - -@PrepareForTest({ InstanceClass.class }) - -@PowerMockIgnore("javax.management.\*") - -Powermockito.whenNew(InstanceClass.class).thenReturn(Object value) -``` - -##### 4.4 模拟静态方法 - -模拟静态方法类似于模拟构造函数,也需要加入注释标记。 - -```java -@RunWith(PowerMockRunner.class) - -@PrepareForTest({ StaticClassToMock.class }) - -@PowerMockIgnore("javax.management.\*") - -Powermockito.mockStatic(StaticClassToMock.class); - - Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) -``` - -##### 4.5 模拟final方法 - -Final方法的模拟类似于模拟静态方法。 - -```java -@RunWith(PowerMockRunner.class) - -@PrepareForTest({ FinalClassToMock.class }) - -@PowerMockIgnore("javax.management.\*") - -Powermockito.mockStatic(FinalClassToMock.class); - - Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) -``` - -##### 4.6 模拟静态类 - -模拟静态类类似于模拟静态方法。 - -##### 4.7 使用spy方法避免执行被测类中的成员函数 - -如被测试类为:TargetClass,想要屏蔽的方法为targetMethod. - -```java -1) PowerMockito.spy(TargetClass.class); - -2) Powemockito.when(TargetClass.targetMethod()).doReturn() - -3) 注意加入 - -@RunWith(PowerMockRunner.class) - -@PrepareForTest(DisplayMoRelationBuilder.class) - -@PowerMockIgnore("javax.management.*") -``` - -##### 4.8 参数匹配器 - -有时我们在处理doMethod(Param param)时,不想进行精确匹配,这时可以使用Mockito提供的模糊匹配方式。 - -如:Mockito.anyInt(),Mockito.anyString() - -##### 4.9 处理public void型的静态方法 - -```java -Powermockito.doNothing.when(T class2mock, String method, … params> -``` - -#### 5. 单元测试用例可选清单 - -输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。 - -- 必传项测试 -- 唯一字段值测试 -- 空值测试 -- 字段只接受允许的字符 -- 负值测试 -- 字段限于字段长度规范 -- 不可能的值 -- 垃圾值测试 -- 检查字段之间的依赖性 -- 等效类划分和边界条件测试 -- 错误和异常处理测试 \ No newline at end of file From c365d999fbff2c03d345b503f746551b3b9ee6d7 Mon Sep 17 00:00:00 2001 From: zhouweixu Date: Fri, 23 Apr 2021 15:33:42 +0800 Subject: [PATCH 198/207] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/beanvalidationdemo/PersonControllerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java b/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java index f020f05..4f7e594 100644 --- a/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java +++ b/source-code/bean-validation-demo/src/test/java/com/example/beanvalidationdemo/PersonControllerTest.java @@ -42,7 +42,7 @@ public void should_check_person_value() throws Exception { .classId("82938390") .region("Shanghai") .phoneNumber("1816313815").build(); - mockMvc.perform(post("/api/person") + mockMvc.perform(post("/api/persons") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(personRequest))) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围")) @@ -53,7 +53,7 @@ public void should_check_person_value() throws Exception { @Test public void should_check_path_variable() throws Exception { - mockMvc.perform(get("/api/person/6") + mockMvc.perform(get("/api/persons/6") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) .andExpect(content().string("getPersonByID.id: 超过 id 的范围了")); @@ -61,7 +61,7 @@ public void should_check_path_variable() throws Exception { @Test public void should_check_request_param_value2() throws Exception { - mockMvc.perform(put("/api/person") + mockMvc.perform(put("/api/persons") .param("name", "snailclimbsnailclimb") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) From 4842b8b92da407c95e5f3959d195b899056b3658 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 21 Apr 2021 22:33:02 +0800 Subject: [PATCH 199/207] Delete spring-security-login-authentication-process.md --- ...g-security-login-authentication-process.md | 196 ------------------ 1 file changed, 196 deletions(-) delete mode 100644 docs/advanced/spring-security-login-authentication-process.md diff --git a/docs/advanced/spring-security-login-authentication-process.md b/docs/advanced/spring-security-login-authentication-process.md deleted file mode 100644 index 89d0328..0000000 --- a/docs/advanced/spring-security-login-authentication-process.md +++ /dev/null @@ -1,196 +0,0 @@ -Spring Security的登录主要是由一系列的过滤器组成,我们如果需要修改登录的校验逻辑,只需要在过滤器链路上添加修改相关的逻辑即可。这里主要通过Spring Security的源码来了解相关的认证登录的逻辑。 - -#### 1.Spring Security的认证流程 - -主要分析: - -1. 认证用户的流程 -2. 如何进行认证校验 -3. 认证成功后怎么获取用户信息 - -具体的过滤器链路如下所示: - -[![cT2G4g.png](https://z3.ax1x.com/2021/04/19/cT2G4g.png)](https://imgtu.com/i/cT2G4g) - -Spring Security的认证流程图如下,认证的主要过程有: - -1. 用户提交用户名和密码,然后通过UsernamePasswordAuthenticationFilter对其进行封装成为UsernamePasswordAuthenticationToken对象,这个是AbstractAuthenticationToken的子类,而AbstractAuthenticationToken又是Authentication的一个实现,所以可以看到后续获取的都是Authentication类型的对象实例; -2. 将第一步的UsernamePasswordAuthenticationToken对象传递给AuthenticationManager; -3. 通过AbstractUserDetailsAuthenticationProvider的默认实现类DaoAuthenticationProvider的retrieveUser方法,这个方法会调用UserDetailsService的loadUserByUsername方法来进行用户名和密码的判断,使用的默认的逻辑进行处理; -4. 将成功认证后的用户信息放入到SecurityContextHolder中,之后可以通过SecurityContext获取用户的相关信息。 - -[![coGpvR.png](https://z3.ax1x.com/2021/04/19/coGpvR.png)](https://imgtu.com/i/coGpvR) - -spring-security源码下载地址: - -```java -https://github.com/spring-projects/spring-security -``` - -#### 2.Spring Security的认证源码分析 - -##### 2.1 搭建项目并访问 - -首先我们搭建一个Spring Security的项目,使用Spring Boot可以很方便的进行集成开发,主要引入如下的依赖即可(当然也可以查看官网,选择合适的版本): - -```java - - org.springframework.boot - spring-boot-starter-security - -``` - -启动项目后会随机生成一个密码串,这里需要复制保存以便登录的时候使用: - -[![coJ0ld.png](https://z3.ax1x.com/2021/04/19/coJ0ld.png)](https://imgtu.com/i/coJ0ld) - -访问登录地址: - -```java -http://localhost:8080/login -``` - -[![coJfpQ.png](https://z3.ax1x.com/2021/04/19/coJfpQ.png)](https://imgtu.com/i/coJfpQ) - -默认的账户名和密码: - -```java -账户名: user -密码: 项目启动时生成的密码串 -``` - -##### 2.2 进行源码分析 - -1. 进行断点后会发现首先进入的是UsernamePasswordAuthenticationFilter的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,会对用户名和密码进行封装成UsernamePasswordAuthenticationToken对象,然后调用this.getAuthenticationManager().authenticate(authRequest)方法进入到AuthenticationManager中。 - - attemptAuthentication方法源码如下所示: - -```java -@Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException { - if (this.postOnly && !request.getMethod().equals("POST")) { - throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); - } - String username = obtainUsername(request); - username = (username != null) ? username : ""; - username = username.trim(); - String password = obtainPassword(request); - password = (password != null) ? password : ""; - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); - // Allow subclasses to set the "details" property - setDetails(request, authRequest); - return this.getAuthenticationManager().authenticate(authRequest); - } -``` - -2. 随后请求进入到WebSecurityConfigurerAdapter的AuthenticationManagerDelegator中,AuthenticationManagerDelegator是AuthenticationManager的一个子类,最后封装成为UsernamePasswordAuthenticationToken对象,供DaoAuthenticationProvider使用。 - - AuthenticationManagerDelegator的源码如下: - - ```java - static final class AuthenticationManagerDelegator implements AuthenticationManager { - private AuthenticationManagerBuilder delegateBuilder; - private AuthenticationManager delegate; - private final Object delegateMonitor = new Object(); - private Set beanNames; - - AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) { - Assert.notNull(delegateBuilder, "delegateBuilder cannot be null"); - Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager"); - ReflectionUtils.makeAccessible(parentAuthMgrField); - this.beanNames = getAuthenticationManagerBeanNames(context); - validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames); - this.delegateBuilder = delegateBuilder; - } - - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - if (this.delegate != null) { - return this.delegate.authenticate(authentication); - } else { - synchronized(this.delegateMonitor) { - if (this.delegate == null) { - this.delegate = (AuthenticationManager)this.delegateBuilder.getObject(); - this.delegateBuilder = null; - } - } - - return this.delegate.authenticate(authentication); - } - } - - private static Set getAuthenticationManagerBeanNames(ApplicationContext applicationContext) { - String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class); - return new HashSet(Arrays.asList(beanNamesForType)); - } - - private static void validateBeanCycle(Object auth, Set beanNames) { - if (auth != null && !beanNames.isEmpty() && auth instanceof Advised) { - TargetSource targetSource = ((Advised)auth).getTargetSource(); - if (targetSource instanceof LazyInitTargetSource) { - LazyInitTargetSource lits = (LazyInitTargetSource)targetSource; - if (beanNames.contains(lits.getTargetBeanName())) { - throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication."); - } - } - } - } - } - ``` - - org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration.AuthenticationManagerDelegator#authenticate - - ```java - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - if (this.delegate != null) { - return this.delegate.authenticate(authentication); - } - synchronized (this.delegateMonitor) { - if (this.delegate == null) { - this.delegate = this.delegateBuilder.getObject(); - this.delegateBuilder = null; - } - } - return this.delegate.authenticate(authentication); - } - ``` - -3. 进入到DaoAuthenticationProvider的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法进行用户的认证,这里的认证主要会调用默认的UserDetailsService对用户名和密码进行校验,如果是使用的类似于Mysql的数据源,其默认的实现是JdbcDaoImpl。 - - org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser - - ```java - @Override - protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) - throws AuthenticationException { - prepareTimingAttackProtection(); - try { - UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); - if (loadedUser == null) { - throw new InternalAuthenticationServiceException( - "UserDetailsService returned null, which is an interface contract violation"); - } - return loadedUser; - } - catch (UsernameNotFoundException ex) { - mitigateAgainstTimingAttack(authentication); - throw ex; - } - catch (InternalAuthenticationServiceException ex) { - throw ex; - } - catch (Exception ex) { - throw new InternalAuthenticationServiceException(ex.getMessage(), ex); - } - } - - ``` - -4. 将上一步认证后的用户实例放入SecurityContextHolder中,至此我们可以很方便的从SecurityContextHolder中获取用户信息,方法如下: - - ```java - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - ``` - - From 57f2cf90807ae4e04956da4c102becd859bed888 Mon Sep 17 00:00:00 2001 From: Sin <42839671+Fenmul@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:19:03 +0800 Subject: [PATCH 200/207] Update spring-bean-validation.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改错别字 「自定以」 补充 「Validator 编程方式手动进行参数验证」 中遗漏代码 --- docs/spring-bean-validation.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/spring-bean-validation.md b/docs/spring-bean-validation.md index dc6323d..5839a28 100644 --- a/docs/spring-bean-validation.md +++ b/docs/spring-bean-validation.md @@ -333,13 +333,17 @@ Validator validate 具体使用情况如下: ```java -ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); -Validator validator = factory.getValidator() -PersonRequest personRequest = PersonRequest.builder().sex("Man22") - .classId("82938390").build(); -Set> violations = validator.validate(personRequest); -// 输出异常信息 -violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); +/** + * 手动校验对象 + */ +@Test +public void check_person_manually() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + PersonRequest personRequest = PersonRequest.builder().sex("Man22") + .classId("82938390").build(); + Set> violations = validator.validate(personRequest); + violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); } ``` @@ -350,7 +354,7 @@ sex 值不在可选范围 name 不能为空 ``` -## 自定以 Validator(实用) +## 自定义 Validator(实用) 如果自带的校验注解无法满足你的需求的话,你还可以自定义实现注解。 @@ -577,4 +581,4 @@ public class PersonService { - `@NotNull`是 JSR 303 Bean 验证批注,它与数据库约束本身无关。 - `@Column(nullable = false)` : 是 JPA 声明列为非空的方法。 -总结来说就是即前者用于验证,而后者则用于指示数据库创建表的时候对表的约束。 \ No newline at end of file +总结来说就是即前者用于验证,而后者则用于指示数据库创建表的时候对表的约束。 From 1344d0e61d5928c29cfd4050ec024baf28efe7ec Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 27 Apr 2021 20:35:13 +0800 Subject: [PATCH 201/207] Update README.md --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 73eeb7f..e1b0fae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +👍推荐[2021最新实战项目源码下载](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100018862&idx=1&sn=858e00b60c6097e3ba061e79be472280&chksm=4ea1856579d60c73224e4d852af6b0188c3ab905069fc28f4b293963fd1ee55d2069fb229848#rd) + +👍[《JavaGuide 面试突击版》PDF 版本](#公众号) 。[图解计算机基础 PDF 版](#优质原创PDF资源) + +书单已经被移动到[awesome-cs](https://github.com/CodingDocs/awesome-cs) 这个仓库。 + + + +

@@ -10,9 +19,13 @@ 公众号 公众号

- **在线阅读** : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) +**开源的目的是为了大家能一起完善,如果你觉得内容有任何需要完善/补充的地方,欢迎提交 issue/pr。** + +- Github地址:https://github.com/CodingDocs/springboot-guide +- 码云地址:https://gitee.com/SnailClimb/springboot-guide(Github无法访问或者访问速度比较慢的小伙伴可以看码云上的对应内容) + ## 重要知识点 ### 基础 @@ -54,11 +67,9 @@ 1. 项目 logo 由 [logoly](https://logoly.pro/#/) 生成。 2. 利用 docsify 生成文档部署在 Github Pages 和 Gitee Pages: [docsify 官网介绍](https://docsify.js.org/#/) -### 联系我 - -添加我的微信备注“Github”,回复关键字 **“加群”** 即可入群。 +### 优质原创PDF资源 -![个人微信](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/wechat3.jpeg) +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-2@main/%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%93%E4%B8%9A/image-20201027160348395.png) ### 公众号 From b4c41a87678724c9219d122a8c44ece8d9084691 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 27 Apr 2021 20:38:37 +0800 Subject: [PATCH 202/207] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e1b0fae..2dda8de 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ 公众号 公众号

+ **在线阅读** : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) **开源的目的是为了大家能一起完善,如果你觉得内容有任何需要完善/补充的地方,欢迎提交 issue/pr。** From b522d00f669c3bb9216c69e6631c6a64e3be3dbf Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 27 Apr 2021 20:56:10 +0800 Subject: [PATCH 203/207] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2dda8de..e1b0fae 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ 公众号 公众号

- **在线阅读** : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) **开源的目的是为了大家能一起完善,如果你觉得内容有任何需要完善/补充的地方,欢迎提交 issue/pr。** From 0c9c92a309293c8a9a235b21133b107a93616ed4 Mon Sep 17 00:00:00 2001 From: guide Date: Tue, 11 May 2021 12:41:19 +0800 Subject: [PATCH 204/207] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.gitignore | 33 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 118 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../springboot-data-desensitization/mvnw | 322 ++++++++++++++++++ .../springboot-data-desensitization/mvnw.cmd | 182 ++++++++++ .../springboot-data-desensitization/pom.xml | 54 +++ ...ingbootDataDesensitizationApplication.java | 13 + .../JsonDesensitizationSerializer.java | 59 ++++ .../annotation/JsonDesensitization.java | 25 ++ .../desensitizer/AbstractDesensitizer.java | 71 ++++ .../desensitizer/Desensitizer.java | 11 + .../desensitizer/DesensitizerFactory.java | 45 +++ .../impl/AddressDesensitizer.java | 15 + .../impl/BankCardDesensitizer.java | 15 + .../impl/BirthdayDesensitizer.java | 15 + .../impl/DefaultDesensitizer.java | 15 + .../desensitizer/impl/EmailDesensitizer.java | 15 + .../desensitizer/impl/IdCardDesensitizer.java | 15 + .../impl/LandlineDesensitizer.java | 15 + .../desensitizer/impl/MobileDesensitizer.java | 15 + .../impl/PasswordDesensitizer.java | 15 + .../enums/DesensitizationType.java | 46 +++ .../exception/DesensitizationException.java | 14 + .../entity/User.java | 35 ++ .../src/main/resources/application.properties | 1 + .../JsonDesensitizationSerializerTest.java | 33 ++ 27 files changed, 1199 insertions(+) create mode 100644 source-code/springboot-data-desensitization/.gitignore create mode 100644 source-code/springboot-data-desensitization/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.jar create mode 100644 source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.properties create mode 100755 source-code/springboot-data-desensitization/mvnw create mode 100644 source-code/springboot-data-desensitization/mvnw.cmd create mode 100644 source-code/springboot-data-desensitization/pom.xml create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/SpringbootDataDesensitizationApplication.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/annotation/JsonDesensitization.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/AbstractDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/Desensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/DesensitizerFactory.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/AddressDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BankCardDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BirthdayDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/DefaultDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/EmailDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/IdCardDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/LandlineDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/MobileDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/PasswordDesensitizer.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/enums/DesensitizationType.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/exception/DesensitizationException.java create mode 100644 source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/entity/User.java create mode 100644 source-code/springboot-data-desensitization/src/main/resources/application.properties create mode 100644 source-code/springboot-data-desensitization/src/test/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializerTest.java diff --git a/source-code/springboot-data-desensitization/.gitignore b/source-code/springboot-data-desensitization/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/source-code/springboot-data-desensitization/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### 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/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/source-code/springboot-data-desensitization/.mvn/wrapper/MavenWrapperDownloader.java b/source-code/springboot-data-desensitization/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/source-code/springboot-data-desensitization/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.jar b/source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.properties b/source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ffdc10e --- /dev/null +++ b/source-code/springboot-data-desensitization/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/source-code/springboot-data-desensitization/mvnw b/source-code/springboot-data-desensitization/mvnw new file mode 100755 index 0000000..3c8a553 --- /dev/null +++ b/source-code/springboot-data-desensitization/mvnw @@ -0,0 +1,322 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="$(/usr/libexec/java_home)" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +if [ -z "$M2_HOME" ]; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then + PRG="$link" + else + PRG="$(dirname "$PRG")/$link" + fi + done + + saveddir=$(pwd) + + M2_HOME=$(dirname "$PRG")/.. + + # make it fully qualified + M2_HOME=$(cd "$M2_HOME" && pwd) + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=$(cygpath --unix "$M2_HOME") + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$M2_HOME" ] && + M2_HOME="$( ( + cd "$M2_HOME" + pwd + ))" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="$( ( + cd "$JAVA_HOME" + pwd + ))" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr \"$javaExecutable\" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! $(expr "$readLink" : '\([^ ]*\)') = "no" ]; then + if $darwin; then + javaHome="$(dirname \"$javaExecutable\")" + javaExecutable="$(cd \"$javaHome\" && pwd -P)/javac" + else + javaExecutable="$(readlink -f \"$javaExecutable\")" + fi + javaHome="$(dirname \"$javaExecutable\")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(which java)" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." + pwd + ) + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' <"$1")" + fi +} + +BASE_DIR=$(find_maven_basedir "$(pwd)") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in wrapperUrl) + jarUrl="$value" + break + ;; + esac + done <"$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=$(cygpath --path --windows "$M2_HOME") + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/source-code/springboot-data-desensitization/mvnw.cmd b/source-code/springboot-data-desensitization/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/source-code/springboot-data-desensitization/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/source-code/springboot-data-desensitization/pom.xml b/source-code/springboot-data-desensitization/pom.xml new file mode 100644 index 0000000..baa65b9 --- /dev/null +++ b/source-code/springboot-data-desensitization/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.5 + + + com.github.javaguide + springboot-data-desensitization + 0.0.1-SNAPSHOT + springboot-data-desensitization + Demo project for Spring Boot + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/SpringbootDataDesensitizationApplication.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/SpringbootDataDesensitizationApplication.java new file mode 100644 index 0000000..8fd6367 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/SpringbootDataDesensitizationApplication.java @@ -0,0 +1,13 @@ +package com.github.springbootdatadesensitization; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootDataDesensitizationApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootDataDesensitizationApplication.class, args); + } + +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializer.java new file mode 100644 index 0000000..8ea4523 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializer.java @@ -0,0 +1,59 @@ +package com.github.springbootdatadesensitization.desensitize; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.github.springbootdatadesensitization.desensitize.annotation.JsonDesensitization; +import com.github.springbootdatadesensitization.desensitize.desensitizer.Desensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.DesensitizerFactory; +import com.github.springbootdatadesensitization.desensitize.enums.DesensitizationType; + +import java.io.IOException; +import java.util.Objects; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/05/10 20:26 + **/ +public class JsonDesensitizationSerializer extends JsonSerializer implements ContextualSerializer { + + + private DesensitizationType type; + + public JsonDesensitizationSerializer() { + } + + public JsonDesensitizationSerializer(DesensitizationType type) { + this.type = type; + } + + @Override + public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty property) throws JsonMappingException { + if (property != null) { + if (Objects.equals(property.getType().getRawClass(), String.class)) { + JsonDesensitization jsonDesensitization = property.getAnnotation(JsonDesensitization.class); + if (jsonDesensitization == null) { + jsonDesensitization = property.getContextAnnotation(JsonDesensitization.class); + } + if (jsonDesensitization != null) { + return new JsonDesensitizationSerializer(jsonDesensitization.value()); + } + } + return serializerProvider.findValueSerializer(property.getType(), property); + } else { + return serializerProvider.findNullValueSerializer(null); + } + } + + @Override + public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + Desensitizer desensitizer = DesensitizerFactory.get(this.type); + System.out.println(s); + jsonGenerator.writeString(desensitizer.desensitize(s)); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/annotation/JsonDesensitization.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/annotation/JsonDesensitization.java new file mode 100644 index 0000000..d7fbf5d --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/annotation/JsonDesensitization.java @@ -0,0 +1,25 @@ +package com.github.springbootdatadesensitization.desensitize.annotation; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.github.springbootdatadesensitization.desensitize.JsonDesensitizationSerializer; +import com.github.springbootdatadesensitization.desensitize.enums.DesensitizationType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/05/10 20:36 + **/ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +@JacksonAnnotationsInside +@JsonSerialize(using = JsonDesensitizationSerializer.class) +public @interface JsonDesensitization { + DesensitizationType value(); +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/AbstractDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/AbstractDesensitizer.java new file mode 100644 index 0000000..32bbcd1 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/AbstractDesensitizer.java @@ -0,0 +1,71 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer; + +import com.github.springbootdatadesensitization.desensitize.exception.DesensitizationException; + +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * 数据脱敏所使用的的策略 + * + * @author Guide哥 + * @date 2021/05/10 20:15 + **/ +public abstract class AbstractDesensitizer implements Desensitizer { + + /** + * 左边的明文长度 + */ + private final Integer leftPlainTextLen; + /** + * 右边的明文长度 + */ + private Integer rightPlainTextLen; + + public AbstractDesensitizer(Integer leftPlainTextLen) { + this.leftPlainTextLen = leftPlainTextLen; + } + + public AbstractDesensitizer(Integer leftPlainTextLen, Integer rightPlainTextLen) { + this.leftPlainTextLen = leftPlainTextLen; + this.rightPlainTextLen = rightPlainTextLen; + } + + @Override + public String desensitize(String origin) { + if (origin == null || origin.isEmpty()) { + return ""; + } + // 处理邮箱的特殊情况 + if (isEmail(origin)) { + int index = origin.indexOf("@"); + rightPlainTextLen = origin.length() - index; + } + if (leftPlainTextLen == 0 && rightPlainTextLen == 0) { + return getEncryptedStr(6); + } + if (leftPlainTextLen < 0 || rightPlainTextLen < 0) { + throw new DesensitizationException("leftPlainTextLen and rightPlainTextLen must > 0"); + } + if (leftPlainTextLen + rightPlainTextLen >= origin.length()) { + throw new DesensitizationException("leftPlainTextLen+rightPlainTextLen should <= the length of origin"); + } + StringBuilder result = new StringBuilder(); + result.append(origin, 0, leftPlainTextLen); + result.append(getEncryptedStr(origin.length() - leftPlainTextLen - rightPlainTextLen)); + result.append(origin, origin.length() - rightPlainTextLen, origin.length()); + return result.toString(); + } + + private Boolean isEmail(String s) { + String emailRegex = "^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$"; + Pattern emailPattern = Pattern.compile(emailRegex); + return emailPattern.matcher(s).matches(); + } + + private String getEncryptedStr(int length) { + return Stream.generate(() -> "*").limit(length).collect(Collectors.joining()); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/Desensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/Desensitizer.java new file mode 100644 index 0000000..721009f --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/Desensitizer.java @@ -0,0 +1,11 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/05/10 20:36 + **/ +public interface Desensitizer { + String desensitize(String origin); +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/DesensitizerFactory.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/DesensitizerFactory.java new file mode 100644 index 0000000..f7d6fb3 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/DesensitizerFactory.java @@ -0,0 +1,45 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.AddressDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.BankCardDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.BirthdayDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.DefaultDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.EmailDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.IdCardDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.LandlineDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.MobileDesensitizer; +import com.github.springbootdatadesensitization.desensitize.desensitizer.impl.PasswordDesensitizer; +import com.github.springbootdatadesensitization.desensitize.enums.DesensitizationType; + +import java.util.HashMap; +import java.util.Map; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/05/10 23:18 + **/ +public class DesensitizerFactory { + public static final Map desensitizers = new HashMap<>(); + + static { + desensitizers.put(DesensitizationType.ADDRESS, new AddressDesensitizer()); + desensitizers.put(DesensitizationType.BANK_CARD, new BankCardDesensitizer()); + desensitizers.put(DesensitizationType.EMAIL, new EmailDesensitizer()); + desensitizers.put(DesensitizationType.ID_CARD, new IdCardDesensitizer()); + desensitizers.put(DesensitizationType.LANDLINE, new LandlineDesensitizer()); + desensitizers.put(DesensitizationType.MOBILE, new MobileDesensitizer()); + desensitizers.put(DesensitizationType.PASSWORD, new PasswordDesensitizer()); + desensitizers.put(DesensitizationType.DEFAULT, new DefaultDesensitizer()); + desensitizers.put(DesensitizationType.BIRTHDAY, new BirthdayDesensitizer()); + } + + public static Desensitizer get(DesensitizationType desensitizationType) { + Desensitizer desensitizer = desensitizers.get(desensitizationType); + if (desensitizer == null) { + return desensitizers.get(DesensitizationType.DEFAULT); + } + return desensitizer; + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/AddressDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/AddressDesensitizer.java new file mode 100644 index 0000000..cf9e429 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/AddressDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 地址脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:15 + **/ +public class AddressDesensitizer extends AbstractDesensitizer { + public AddressDesensitizer() { + super(3, 3); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BankCardDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BankCardDesensitizer.java new file mode 100644 index 0000000..b9620f9 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BankCardDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 银行卡号脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class BankCardDesensitizer extends AbstractDesensitizer { + public BankCardDesensitizer() { + super(3, 3); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BirthdayDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BirthdayDesensitizer.java new file mode 100644 index 0000000..1f8d300 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/BirthdayDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 密码脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class BirthdayDesensitizer extends AbstractDesensitizer { + public BirthdayDesensitizer() { + super(4, 0); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/DefaultDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/DefaultDesensitizer.java new file mode 100644 index 0000000..9a833de --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/DefaultDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 密码脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class DefaultDesensitizer extends AbstractDesensitizer { + public DefaultDesensitizer() { + super(0, 0); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/EmailDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/EmailDesensitizer.java new file mode 100644 index 0000000..3ea0fb4 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/EmailDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 邮箱脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class EmailDesensitizer extends AbstractDesensitizer { + public EmailDesensitizer() { + super(0); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/IdCardDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/IdCardDesensitizer.java new file mode 100644 index 0000000..d979913 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/IdCardDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 身份证号脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class IdCardDesensitizer extends AbstractDesensitizer { + public IdCardDesensitizer() { + super(3, 4); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/LandlineDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/LandlineDesensitizer.java new file mode 100644 index 0000000..5bec86e --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/LandlineDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 座机号脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class LandlineDesensitizer extends AbstractDesensitizer { + public LandlineDesensitizer() { + super(2, 2); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/MobileDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/MobileDesensitizer.java new file mode 100644 index 0000000..2482a9c --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/MobileDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 手机号脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class MobileDesensitizer extends AbstractDesensitizer { + public MobileDesensitizer() { + super(3, 4); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/PasswordDesensitizer.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/PasswordDesensitizer.java new file mode 100644 index 0000000..62639ff --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/desensitizer/impl/PasswordDesensitizer.java @@ -0,0 +1,15 @@ +package com.github.springbootdatadesensitization.desensitize.desensitizer.impl; + +import com.github.springbootdatadesensitization.desensitize.desensitizer.AbstractDesensitizer; + +/** + * 密码脱敏 + * + * @author Guide哥 + * @date 2021/05/10 20:28 + **/ +public class PasswordDesensitizer extends AbstractDesensitizer { + public PasswordDesensitizer() { + super(0, 0); + } +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/enums/DesensitizationType.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/enums/DesensitizationType.java new file mode 100644 index 0000000..502cf70 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/enums/DesensitizationType.java @@ -0,0 +1,46 @@ +package com.github.springbootdatadesensitization.desensitize.enums; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/05/10 20:36 + **/ +public enum DesensitizationType { + + DEFAULT, + /** + * 座机号 + */ + LANDLINE, + /** + * 手机 + */ + MOBILE, + /** + * 邮箱 + */ + EMAIL, + + /** + * 生日🎂 + */ + BIRTHDAY, + /** + * 密码 + */ + PASSWORD, + /** + * 身份证 + */ + ID_CARD, + /** + * 银行卡 + */ + BANK_CARD, + /** + * 地址 + */ + ADDRESS, + +} diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/exception/DesensitizationException.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/exception/DesensitizationException.java new file mode 100644 index 0000000..50a70b4 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/desensitize/exception/DesensitizationException.java @@ -0,0 +1,14 @@ +package com.github.springbootdatadesensitization.desensitize.exception; + +import lombok.Getter; + +/** + * @author shuang.kou + * @date 2017/12/19 14:25 + */ +@Getter +public class DesensitizationException extends RuntimeException { + public DesensitizationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/entity/User.java b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/entity/User.java new file mode 100644 index 0000000..6dcbda5 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/java/com/github/springbootdatadesensitization/entity/User.java @@ -0,0 +1,35 @@ +package com.github.springbootdatadesensitization.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.github.springbootdatadesensitization.desensitize.annotation.JsonDesensitization; +import com.github.springbootdatadesensitization.desensitize.enums.DesensitizationType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + private Long id; + @JsonDesensitization(DesensitizationType.EMAIL) + private String email; + + @JsonDesensitization(DesensitizationType.ID_CARD) + private String idCard; + + @JsonDesensitization(DesensitizationType.PASSWORD) + private String password; + + @JsonDesensitization(DesensitizationType.MOBILE) + private String phone; + + @JsonFormat(pattern = "yyyy-MM-dd", locale = "zh", timezone = "GMT+8") + @JsonDesensitization(DesensitizationType.BIRTHDAY) + private Date birthday; +} diff --git a/source-code/springboot-data-desensitization/src/main/resources/application.properties b/source-code/springboot-data-desensitization/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/source-code/springboot-data-desensitization/src/test/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializerTest.java b/source-code/springboot-data-desensitization/src/test/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializerTest.java new file mode 100644 index 0000000..3debd98 --- /dev/null +++ b/source-code/springboot-data-desensitization/src/test/java/com/github/springbootdatadesensitization/desensitize/JsonDesensitizationSerializerTest.java @@ -0,0 +1,33 @@ +package com.github.springbootdatadesensitization.desensitize; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.springbootdatadesensitization.entity.User; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JsonDesensitizationSerializerTest { + + @Test + void should_desensitize_entity() throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + User user = User.builder() + .birthday(new Date()) + .phone("1816313814") + .email("136148@qq.com") + .password("12dsfs@dfsdg431") + .idCard("463827319960397495") + .build(); + String s = objectMapper.writeValueAsString(user); + System.out.println(s); + User desensitizedUser = objectMapper.readValue(s, User.class); + assertEquals("******@qq.com", desensitizedUser.getEmail()); + assertEquals("463***********7495", desensitizedUser.getIdCard()); + assertEquals("******", desensitizedUser.getPassword()); + assertEquals("181***3814", desensitizedUser.getPhone()); + + } +} \ No newline at end of file From 5a184f795d1e554f95dade65359c8fcbaf969a7a Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 19 May 2021 08:58:45 +0800 Subject: [PATCH 205/207] =?UTF-8?q?MyBatis-Plus=20=20=E4=BB=8E=E5=85=A5?= =?UTF-8?q?=E9=97=A8=E5=88=B0=E4=B8=8A=E6=89=8B=E5=B9=B2=E4=BA=8B=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +- docs/MyBatisPlus.md | 975 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 979 insertions(+), 3 deletions(-) create mode 100644 docs/MyBatisPlus.md diff --git a/README.md b/README.md index e1b0fae..025fee7 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,10 @@ 4. [Spring 如何优雅读取配置文件?](./docs/basis/read-config-properties.md) 5. **异常处理** :[Spring Boot 异常处理的几种方式](./docs/advanced/springboot-handle-exception.md)、[Spring Boot 异常处理在实际项目中的应用](./docs/advanced/springboot-handle-exception-plus.md) 6. **JPA** : [ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md) 、 [JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md) -7. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) -8. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~) -9. [SpringBoot 2.0+ 集成 Swagger 官方 Starter + knife4j 增强方案](./docs/basis/swagger.md) +7. [MyBatis-Plus 从入门到上手干事!](./docs/MyBatisPlus.md) +8. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) +9. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~) +10. [SpringBoot 2.0+ 集成 Swagger 官方 Starter + knife4j 增强方案](./docs/basis/swagger.md) ### 进阶 diff --git a/docs/MyBatisPlus.md b/docs/MyBatisPlus.md new file mode 100644 index 0000000..afd4043 --- /dev/null +++ b/docs/MyBatisPlus.md @@ -0,0 +1,975 @@ +`MyBatis` 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射,而实际开发中,我们都会选择使用 `MyBatisPlus`,它是对 `MyBatis` 框架的进一步增强,能够极大地简化我们的持久层代码,下面就一起来看看 `MyBatisPlus` 中的一些奇淫巧技吧。 + +> 说明:本篇文章需要一定的 `MyBatis` 与 `MyBatisPlus` 基础 + +MyBatis-Plus 官网地址 : https://baomidou.com/ 。 + +## CRUD + +使用 `MyBatisPlus` 实现业务的增删改查非常地简单,一起来看看吧。 + +**1.首先新建一个 SpringBoot 工程,然后引入依赖:** + +```xml + + com.baomidou + mybatis-plus-boot-starter + 3.4.2 + + + mysql + mysql-connector-java + runtime + + + org.projectlombok + lombok + +``` + +**2.配置一下数据源:** + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + url: jdbc:mysql:///mybatisplus?serverTimezone=UTC + password: 123456 +``` + +**3.创建一下数据表:** + +```sql +CREATE DATABASE `mybatisplus`; + +USE `mybatisplus`; + +DROP TABLE IF EXISTS `tbl_employee`; + +CREATE TABLE `tbl_employee` ( + `id` bigint(20) NOT NULL, + `last_name` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `gender` char(1) DEFAULT NULL, + `age` int(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=gbk; + +insert into `tbl_employee`(`id`,`last_name`,`email`,`gender`,`age`) values (1,'jack','jack@qq.com','1',35),(2,'tom','tom@qq.com','1',30),(3,'jerry','jerry@qq.com','1',40); +``` + +**4.创建对应的实体类:** + +```java +@Data +public class Employee { + + private Long id; + private String lastName; + private String email; + private Integer age; +} +``` + +**4.编写 `Mapper` 接口:** + +```java +public interface EmployeeMapper extends BaseMapper { +} +``` + +我们只需继承 `MyBatisPlus` 提供的 `BaseMapper` 接口即可,现在我们就拥有了对 `Employee` 进行增删改查的 API,比如: + +```java +@SpringBootTest +@MapperScan("com.wwj.mybatisplusdemo.mapper") +class MybatisplusDemoApplicationTests { + + @Autowired + private EmployeeMapper employeeMapper; + + @Test + void contextLoads() { + List employees = employeeMapper.selectList(null); + employees.forEach(System.out::println); + } +} +``` + +运行结果: + +```java +org.springframework.jdbc.BadSqlGrammarException: +### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Table 'mybatisplus.employee' doesn't exist +``` + +程序报错了,原因是不存在 `employee` 表,这是因为我们的实体类名为 `Employee`,`MyBatisPlus` 默认是以类名作为表名进行操作的,可如果类名和表名不相同(实际开发中也确实可能不同),就需要在实体类中使用 `@TableName` 注解来声明表的名称: + +```java +@Data +@TableName("tbl_employee") // 声明表名称 +public class Employee { + + private Long id; + private String lastName; + private String email; + private Integer age; +} +``` + +重新执行测试代码,结果如下: + +```java +Employee(id=1, lastName=jack, email=jack@qq.com, age=35) +Employee(id=2, lastName=tom, email=tom@qq.com, age=30) +Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40) +``` + +`BaseMapper` 提供了常用的一些增删改查方法: + +![](https://img-blog.csdnimg.cn/20210519084059865.png) + +具体细节可以查阅其源码自行体会,注释都是中文的,非常容易理解。 + +在开发过程中,我们通常会使用 `Service` 层来调用 `Mapper` 层的方法,而 `MyBatisPlus` 也为我们提供了通用的 `Service`: + +```java +public interface EmployeeService extends IService { +} + +@Service +public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService { +} +``` + +事实上,我们只需让 `EmployeeServiceImpl` 继承 `ServiceImpl` 即可获得 `Service` 层的方法,**那么为什么还需要实现 `EmployeeService` 接口呢?** + +这是因为实现 `EmployeeService` 接口能够更方便地对业务进行扩展,一些复杂场景下的数据处理,`MyBatisPlus` 提供的 `Service` 方法可能无法处理,此时我们就需要自己编写代码,这时候只需在 `EmployeeService` 中定义自己的方法,并在 `EmployeeServiceImpl` 中实现即可。 + +先来测试一下 `MyBatisPlus` 提供的 `Service` 方法: + +```java +@SpringBootTest +@MapperScan("com.wwj.mybatisplusdemo.mapper") +class MybatisplusDemoApplicationTests { + + @Autowired + private EmployeeService employeeService; + + @Test + void contextLoads() { + List list = employeeService.list(); + list.forEach(System.out::println); + } +} +``` + +运行结果: + +```java +Employee(id=1, lastName=jack, email=jack@qq.com, age=35) +Employee(id=2, lastName=tom, email=tom@qq.com, age=30) +Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40) +``` + +接下来模拟一个自定义的场景,我们来编写自定义的操作方法,首先在 `EmployeeMapper` 中进行声明: + +```java +public interface EmployeeMapper extends BaseMapper { + + List selectAllByLastName(@Param("lastName") String lastName); +} +``` + +此时我们需要自己编写配置文件实现该方法,在 `resource` 目录下新建一个 `mapper` 文件夹,然后在该文件夹下创建 `EmployeeMapper.xml` 文件: + +```xml + + + + + + id, last_name, email, gender, age + + + + +``` + +`MyBatisPlus` 默认扫描的是类路径下的 `mapper` 目录,这可以从源码中得到体现: + +![](https://img-blog.csdnimg.cn/20210519084046472.png) + +所以我们直接将 `Mapper` 配置文件放在该目录下就没有任何问题,可如果不是这个目录,我们就需要进行配置,比如: + +```yaml +mybatis-plus: + mapper-locations: classpath:xml/*.xml +``` + +编写好 `Mapper` 接口后,我们就需要定义 `Service` 方法了: + +```java +public interface EmployeeService extends IService { + + List listAllByLastName(String lastName); +} + +@Service +public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService { + + @Override + public List listAllByLastName(String lastName) { + return baseMapper.selectAllByLastName(lastName); + } +} +``` + +在 `EmployeeServiceImpl` 中我们无需将 `EmployeeMapper` 注入进来,而是使用 `BaseMapper`,查看 `ServiceImpl` 的源码: + +![](https://img-blog.csdnimg.cn/2021051908403249.png) + +可以看到它为我们注入了一个 `BaseMapper` 对象,而它是第一个泛型类型,也就是 `EmployeeMapper` 类型,所以我们可以直接使用这个 `baseMapper` 来调用 `Mapper` 中的方法,此时编写测试代码: + +```java +@SpringBootTest +@MapperScan("com.wwj.mybatisplusdemo.mapper") +class MybatisplusDemoApplicationTests { + + @Autowired + private EmployeeService employeeService; + + @Test + void contextLoads() { + List list = employeeService.listAllByLastName("tom"); + list.forEach(System.out::println); + } +} +``` + +运行结果: + +```java +Employee(id=2, lastName=tom, email=tom@qq.com, age=30) +``` + +## ID 策略 + +在创建表的时候我故意没有设置主键的增长策略,现在我们来插入一条数据,看看主键是如何增长的: + +```java +@Test +void contextLoads() { + Employee employee = new Employee(); + employee.setLastName("lisa"); + employee.setEmail("lisa@qq.com"); + employee.setAge(20); + employeeService.save(employee); +} +``` + +插入成功后查询一下数据表: + +```sql +mysql> select * from tbl_employee; ++---------------------+-----------+--------------+--------+------+ +| id | last_name | email | gender | age | ++---------------------+-----------+--------------+--------+------+ +| 1 | jack | jack@qq.com | 1 | 35 | +| 2 | tom | tom@qq.com | 1 | 30 | +| 3 | jerry | jerry@qq.com | 1 | 40 | +| 1385934720849584129 | lisa | lisa@qq.com | NULL | 20 | ++---------------------+-----------+--------------+--------+------+ +4 rows in set (0.00 sec) +``` + +可以看到 id 是一串相当长的数字,这是什么意思呢?提前剧透一下,这其实是分布式 id,那又何为分布式 id 呢? + +我们知道,对于一个大型应用,其访问量是非常巨大的,就比如说一个网站每天都有人进行注册,注册的用户信息就需要存入数据表,随着日子一天天过去,数据表中的用户越来越多,此时数据库的查询速度就会受到影响,所以一般情况下,当数据量足够庞大时,数据都会做分库分表的处理。 + +然而,一旦分表,问题就产生了,很显然这些分表的数据都是属于同一张表的数据,只是因为数据量过大而分成若干张表,那么这几张表的主键 id 该怎么管理呢?每张表维护自己的 id?那数据将会有很多的 id 重复,这当然是不被允许的,其实,我们可以使用算法来生成一个绝对不会重复的 id,这样问题就迎刃而解了,事实上,分布式 id 的解决方案有很多: + +1. UUID +1. SnowFlake +1. TinyID +1. Uidgenerator +1. Leaf +6. Tinyid +7. ...... + +以 UUID 为例,它生成的是一串由数字和字母组成的字符串,显然并不适合作为数据表的 id,而且 id 保持递增有序会加快表的查询效率,基于此,`MyBatisPlus` 使用的就是 `SnowFlake`(雪花算法)。 + +`Snowflake` 是 Twitter 开源的分布式 ID 生成算法。`Snowflake` 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义: + +- **第 0 位**: 符号位(标识正负),始终为 0,没有用,不用管。 +- **第 1~41 位** :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年) +- **第 42~52 位** :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。 +- **第 53~64 位** :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。 + +![](https://oscimg.oschina.net/oscnet/up-a7e54a77b5ab1d9fa16d5ae3a3c50c5aee9.png) + +这也就是为什么插入数据后新的数据 id 是一长串数字的原因了,我们可以在实体类中使用 `@TableId` 来设置主键的策略: + +```java +@Data +@TableName("tbl_employee") +public class Employee { + + @TableId(type = IdType.AUTO) // 设置主键策略 + private Long id; + private String lastName; + private String email; + private Integer age; +} +``` + +`MyBatisPlus` 提供了几种主键的策略: +![](https://img-blog.csdnimg.cn/20210519084011745.png) +其中 `AUTO` 表示数据库自增策略,该策略下需要数据库实现主键的自增(auto_increment),`ASSIGN_ID` 是雪花算法,默认使用的是该策略,`ASSIGN_UUID` 是 UUID 策略,一般不会使用该策略。 + +这里多说一点, 当实体类的主键名为 id,并且数据表的主键名也为 id 时,此时 `MyBatisPlus` 会自动判定该属性为主键 id,倘若名字不是 id 时,就需要标注 `@TableId` 注解,若是实体类中主键名与数据表的主键名不一致,则可以进行声明: + +```java +@TableId(value = "uid",type = IdType.AUTO) // 设置主键策略 +private Long id; +``` + +还可以在配置文件中配置全局的主键策略: + +```yaml +mybatis-plus: + global-config: + db-config: + id-type: auto +``` + +这样能够避免在每个实体类中重复设置主键策略。 + +## 属性自动填充 + +翻阅《阿里巴巴Java开发手册》,在第 5 章 MySQL 数据库可以看到这样一条规范: +![](https://img-blog.csdnimg.cn/20210426214821389.png) +对于一张数据表,它必须具备三个字段: + +- `id` : 唯一ID +- `gmt_create` : 保存的是当前数据创建的时间 +- `gmt_modified` : 保存的是更新时间 + +我们改造一下数据表: + +```sql +alter table tbl_employee add column gmt_create datetime not null; +alter table tbl_employee add column gmt_modified datetime not null; +``` + +然后改造一下实体类: + +```java +@Data +@TableName("tbl_employee") +public class Employee { + + @TableId(type = IdType.AUTO) // 设置主键策略 + private Long id; + private String lastName; + private String email; + private Integer age; + private LocalDateTime gmtCreate; + private LocalDateTime gmtModified; +} +``` + +此时我们在插入数据和更新数据的时候就需要手动去维护这两个属性: + +```java +@Test +void contextLoads() { + Employee employee = new Employee(); + employee.setLastName("lisa"); + employee.setEmail("lisa@qq.com"); + employee.setAge(20); + // 设置创建时间 + employee.setGmtCreate(LocalDateTime.now()); + employee.setGmtModified(LocalDateTime.now()); + employeeService.save(employee); +} + +@Test +void contextLoads() { + Employee employee = new Employee(); + employee.setId(1385934720849584130L); + employee.setAge(50); + // 设置创建时间 + employee.setGmtModified(LocalDateTime.now()); + employeeService.updateById(employee); +} +``` + +每次都需要维护这两个属性未免过于麻烦,好在 `MyBatisPlus` 提供了字段自动填充功能来帮助我们进行管理,需要使用到的是 `@TableField` 注解: + +```java +@Data +@TableName("tbl_employee") +public class Employee { + + @TableId(type = IdType.AUTO) + private Long id; + private String lastName; + private String email; + private Integer age; + @TableField(fill = FieldFill.INSERT) // 插入的时候自动填充 + private LocalDateTime gmtCreate; + @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新的时候自动填充 + private LocalDateTime gmtModified; +} +``` + +然后编写一个类实现 MetaObjectHandler 接口: + +```java +@Component +@Slf4j +public class MyMetaObjectHandler implements MetaObjectHandler { + + /** + * 实现插入时的自动填充 + * @param metaObject + */ + @Override + public void insertFill(MetaObject metaObject) { + log.info("insert开始属性填充"); + this.strictInsertFill(metaObject,"gmtCreate", LocalDateTime.class,LocalDateTime.now()); + this.strictInsertFill(metaObject,"gmtModified", LocalDateTime.class,LocalDateTime.now()); + } + + /** + * 实现更新时的自动填充 + * @param metaObject + */ + @Override + public void updateFill(MetaObject metaObject) { + log.info("update开始属性填充"); + this.strictInsertFill(metaObject,"gmtModified", LocalDateTime.class,LocalDateTime.now()); + } +} +``` + +该接口中有两个未实现的方法,分别为插入和更新时的填充方法,在方法中调用 `strictInsertFill()` 方法 即可实现属性的填充,它需要四个参数: + +1. `metaObject`:元对象,就是方法的入参 +1. `fieldName`:为哪个属性进行自动填充 +1. `fieldType`:属性的类型 +1. `fieldVal`:需要填充的属性值 + +此时在插入和更新数据之前,这两个方法会先被执行,以实现属性的自动填充,通过日志我们可以进行验证: + +```java +@Test +void contextLoads() { + Employee employee = new Employee(); + employee.setId(1385934720849584130L); + employee.setAge(15); + employeeService.updateById(employee); +} +``` + +运行结果: + +```java +INFO 15584 --- [ main] c.w.m.handler.MyMetaObjectHandler : update开始属性填充 +2021-04-24 21:32:19.788 INFO 15584 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2021-04-24 21:32:21.244 INFO 15584 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +``` + +属性填充其实可以进行一些优化,考虑一些特殊情况,对于一些不存在的属性,就不需要进行属性填充,对于一些设置了值的属性,也不需要进行属性填充,这样可以提高程序的整体运行效率: + +```java +@Component +@Slf4j +public class MyMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + boolean hasGmtCreate = metaObject.hasSetter("gmtCreate"); + boolean hasGmtModified = metaObject.hasSetter("gmtModified"); + if (hasGmtCreate) { + Object gmtCreate = this.getFieldValByName("gmtCreate", metaObject); + if (gmtCreate == null) { + this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.class, LocalDateTime.now()); + } + } + if (hasGmtModified) { + Object gmtModified = this.getFieldValByName("gmtModified", metaObject); + if (gmtModified == null) { + this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now()); + } + } + } + + @Override + public void updateFill(MetaObject metaObject) { + boolean hasGmtModified = metaObject.hasSetter("gmtModified"); + if (hasGmtModified) { + Object gmtModified = this.getFieldValByName("gmtModified", metaObject); + if (gmtModified == null) { + this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now()); + } + } + } +} +``` + +## 逻辑删除 + +逻辑删除对应的是物理删除,分别介绍一下这两个概念: + +1. **物理删除** :指的是真正的删除,即:当执行删除操作时,将数据表中的数据进行删除,之后将无法再查询到该数据 +1. **逻辑删除** :并不是真正意义上的删除,只是对于用户不可见了,它仍然存在与数据表中 + +在这个数据为王的时代,数据就是财富,所以一般并不会有哪个系统在删除某些重要数据时真正删掉了数据,通常都是在数据库中建立一个状态列,让其默认为 0,当为 0 时,用户可见;当执行了删除操作,就将状态列改为 1,此时用户不可见,但数据还是在表中的。 + +![](https://img-blog.csdnimg.cn/20210426215107210.png) + +按照《阿里巴巴Java开发手册》第 5 章 MySQL 数据库相关的建议,我们来为数据表新增一个`is_deleted` 字段: + +```sql +alter table tbl_employee add column is_deleted tinyint not null; +``` + +在实体类中也要添加这一属性: + +```java +@Data +@TableName("tbl_employee") +public class Employee { + + @TableId(type = IdType.AUTO) + private Long id; + private String lastName; + private String email; + private Integer age; + @TableField(fill = FieldFill.INSERT) + private LocalDateTime gmtCreate; + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime gmtModified; + /** + * 逻辑删除属性 + */ + @TableLogic + @TableField("is_deleted") + private Boolean deleted; +} +``` + +![](https://img-blog.csdnimg.cn/2021042621530162.png) + +还是参照《阿里巴巴Java开发手册》第 5 章 MySQL 数据库相关的建议,对于布尔类型变量,不能加 is 前缀,所以我们的属性被命名为 `deleted`,但此时就无法与数据表的字段进行对应了,所以我们需要使用 `@TableField` 注解来声明一下数据表的字段名,而 `@TableLogin` 注解用于设置逻辑删除属性;此时我们执行删除操作: + +```java +@Test +void contextLoads() { + employeeService.removeById(3); +} +``` + +查询数据表: + +```sql +mysql> select * from tbl_employee; ++---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+ +| id | last_name | email | gender | age | gmt_create | gmt_modified | is_deleted | ++---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+ +| 1 | jack | jack@qq.com | 1 | 35 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 | +| 2 | tom | tom@qq.com | 1 | 30 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 | +| 3 | jerry | jerry@qq.com | 1 | 40 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 1 | +| 1385934720849584129 | lisa | lisa@qq.com | NULL | 20 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 | +| 1385934720849584130 | lisa | lisa@qq.com | NULL | 15 | 2021-04-24 21:14:18 | 2021-04-24 21:32:19 | 0 | ++---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+ +5 rows in set (0.00 sec) +``` + +可以看到数据并没有被删除,只是 `is_deleted` 字段的属性值被更新成了 1,此时我们再来执行查询操作: + +```java +@Test +void contextLoads() { + List list = employeeService.list(); + list.forEach(System.out::println); +} +``` + +执行结果: + +```java +Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false) +Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false) +Employee(id=1385934720849584129, lastName=lisa, email=lisa@qq.com, age=20, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false) +Employee(id=1385934720849584130, lastName=lisa, email=lisa@qq.com, age=15, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:32:19, deleted=false) +``` + +会发现第三条数据并没有被查询出来,它是如何实现的呢?我们可以输出 `MyBatisPlus` 生成的 SQL 来分析一下,在配置文件中进行配置: + +```yaml +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出SQL日志 +``` + +运行结果: + +```java +==> Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0 +==> Parameters: +<== Columns: id, last_name, email, age, gmt_create, gmt_modified, deleted +<== Row: 1, jack, jack@qq.com, 35, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0 +<== Row: 2, tom, tom@qq.com, 30, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0 +<== Row: 1385934720849584129, lisa, lisa@qq.com, 20, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0 +<== Row: 1385934720849584130, lisa, lisa@qq.com, 15, 2021-04-24 21:14:18, 2021-04-24 21:32:19, 0 +<== Total: 4 +``` + +原来它在查询时携带了一个条件: `is_deleted=0` ,这也说明了 `MyBatisPlus` 默认 0 为不删除,1 为删除。 +若是你想修改这个规定,比如设置-1 为删除,1 为不删除,也可以进行配置: + +```yaml +mybatis-plus: + global-config: + db-config: + id-type: auto + logic-delete-field: deleted # 逻辑删除属性名 + logic-delete-value: -1 # 删除值 + logic-not-delete-value: 1 # 不删除值 +``` + +但建议使用默认的配置,阿里巴巴开发手册也规定 1 表示删除,0 表示未删除。 + +## 分页插件 + +对于分页功能,`MyBatisPlus` 提供了分页插件,只需要进行简单的配置即可实现: + +```java +@Configuration +public class MyBatisConfig { + + /** + * 注册分页插件 + * @return + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } +} +``` + +接下来我们就可以使用分页插件提供的功能了: + +```java +@Test +void contextLoads() { + Page page = new Page<>(1,2); + employeeService.page(page, null); + List employeeList = page.getRecords(); + employeeList.forEach(System.out::println); + System.out.println("获取总条数:" + page.getTotal()); + System.out.println("获取当前页码:" + page.getCurrent()); + System.out.println("获取总页码:" + page.getPages()); + System.out.println("获取每页显示的数据条数:" + page.getSize()); + System.out.println("是否有上一页:" + page.hasPrevious()); + System.out.println("是否有下一页:" + page.hasNext()); +} +``` + +其中的 `Page` 对象用于指定分页查询的规则,这里表示按每页两条数据进行分页,并查询第一页的内容,运行结果: + +```java +Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0) +Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0) +获取总条数:4 +获取当前页码:1 +获取总页码:2 +获取每页显示的数据条数:2 +是否有上一页:false +是否有下一页:true +``` + +倘若在分页过程中需要限定一些条件,我们就需要构建 QueryWrapper 来实现: + +```java +@Test +void contextLoads() { + Page page = new Page<>(1, 2); + employeeService.page(page, new QueryWrapper() + .between("age", 20, 50) + .eq("gender", 1)); + List employeeList = page.getRecords(); + employeeList.forEach(System.out::println); +} +``` + +此时分页的数据就应该是年龄在 20~50 岁之间,且 gender 值为 1 的员工信息,然后再对这些数据进行分页。 + +## 乐观锁 + +当程序中出现并发访问时,就需要保证数据的一致性。以商品系统为例,现在有两个管理员均想对同一件售价为 100 元的商品进行修改,A 管理员正准备将商品售价改为 150 元,但此时出现了网络问题,导致 A 管理员的操作陷入了等待状态;此时 B 管理员也进行修改,将商品售价改为了 200 元,修改完成后 B 管理员退出了系统,此时 A 管理员的操作也生效了,这样便使得 A 管理员的操作直接覆盖了 B 管理员的操作,B 管理员后续再进行查询时会发现商品售价变为了 150 元,这样的情况是绝对不允许发生的。 + +要想解决这一问题,可以给数据表加锁,常见的方式有两种: + +1. 乐观锁 +1. 悲观锁 + +悲观锁认为并发情况一定会发生,所以在某条数据被修改时,为了避免其它人修改,会直接对数据表进行加锁,它依靠的是数据库本身提供的锁机制(表锁、行锁、读锁、写锁)。 + +而乐观锁则相反,它认为数据产生冲突的情况一般不会发生,所以在修改数据的时候并不会对数据表进行加锁的操作,而是在提交数据时进行校验,判断提交上来的数据是否会发生冲突,如果发生冲突,则提示用户重新进行操作,一般的实现方式为 `设置版本号字段` 。 + +就以商品售价为例,在该表中设置一个版本号字段,让其初始为 1,此时 A 管理员和 B 管理员同时需要修改售价,它们会先读取到数据表中的内容,此时两个管理员读取到的版本号都为 1,此时 B 管理员的操作先生效了,它就会将当前数据表中对应数据的版本号与最开始读取到的版本号作一个比对,发现没有变化,于是修改就生效了,此时版本号加 1。 + +而 A 管理员马上也提交了修改操作,但是此时的版本号为 2,与最开始读取到的版本号并不对应,这就说明数据发生了冲突,此时应该提示 A 管理员操作失败,并让 A 管理员重新查询一次数据。 + +![](https://img-blog.csdnimg.cn/20210426221408623.png) + +乐观锁的优势在于采取了更加宽松的加锁机制,能够提高程序的吞吐量,适用于读操作多的场景。 + +那么接下来我们就来模拟这一过程。 + +**1.创建一张新的数据表:** + +```sql +create table shop( + id bigint(20) not null auto_increment, + name varchar(30) not null, + price int(11) default 0, + version int(11) default 1, + primary key(id) +); + +insert into shop(id,name,price) values(1,'笔记本电脑',8000); +``` + +**2.创建实体类:** + +```java +@Data +public class Shop { + + private Long id; + private String name; + private Integer price; + private Integer version; +} +``` + +**3.创建对应的 `Mapper` 接口:** + +```java +public interface ShopMapper extends BaseMapper { +} +``` + +**4.编写测试代码:** + +```java +@SpringBootTest +@MapperScan("com.wwj.mybatisplusdemo.mapper") +class MybatisplusDemoApplicationTests { + + @Autowired + private ShopMapper shopMapper; + + /** + * 模拟并发场景 + */ + @Test + void contextLoads() { + // A、B管理员读取数据 + Shop A = shopMapper.selectById(1L); + Shop B = shopMapper.selectById(1L); + // B管理员先修改 + B.setPrice(9000); + int result = shopMapper.updateById(B); + if (result == 1) { + System.out.println("B管理员修改成功!"); + } else { + System.out.println("B管理员修改失败!"); + } + // A管理员后修改 + A.setPrice(8500); + int result2 = shopMapper.updateById(A); + if (result2 == 1) { + System.out.println("A管理员修改成功!"); + } else { + System.out.println("A管理员修改失败!"); + } + // 最后查询 + System.out.println(shopMapper.selectById(1L)); + } +} +``` + +执行结果: + +```java +B管理员修改成功! +A管理员修改成功! +Shop(id=1, name=笔记本电脑, price=8500, version=1) +``` + +**问题出现了,B 管理员的操作被 A 管理员覆盖,那么该如何解决这一问题呢?** + +其实 `MyBatisPlus` 已经提供了乐观锁机制,只需要在实体类中使用 `@Version` 声明版本号属性: + +```java +@Data +public class Shop { + + private Long id; + private String name; + private Integer price; + @Version // 声明版本号属性 + private Integer version; +} +``` + +然后注册乐观锁插件: + +```java +@Configuration +public class MyBatisConfig { + + /** + * 注册插件 + * @return + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + // 乐观锁插件 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + return interceptor; + } +} +``` + +重新执行测试代码,结果如下: + +```java +B管理员修改成功! +A管理员修改失败! +Shop(id=1, name=笔记本电脑, price=9000, version=2) +``` + +此时 A 管理员的修改就失败了,它需要重新读取最新的数据才能再次进行修改。 + +## 条件构造器 + +在分页插件中我们简单地使用了一下条件构造器(`Wrapper`),下面我们来详细了解一下。 +先来看看 `Wrapper` 的继承体系: +![](https://img-blog.csdnimg.cn/20210519083944315.png) +分别介绍一下它们的作用: + +- `Wrapper`:条件构造器抽象类,最顶端的父类 + - `AbstractWrapper`:查询条件封装抽象类,生成 SQL 的 where 条件 + - `QueryWrapper`:用于对象封装 + - `UpdateWrapper`:用于条件封装 + - `AbstractLambdaWrapper`:Lambda 语法使用 Wrapper + - `LambdaQueryWrapper`:用于对象封装,使用 Lambda 语法 + - `LambdaUpdateWrapper`:用于条件封装,使用 Lambda 语法 + +通常我们使用的都是 `QueryWrapper` 和 `UpdateWrapper`,若是想使用 Lambda 语法来编写,也可以使用 `LambdaQueryWrapper` 和 `LambdaUpdateWrapper`,通过这些条件构造器,我们能够很方便地来实现一些复杂的筛选操作,比如: + +```java +@SpringBootTest +@MapperScan("com.wwj.mybatisplusdemo.mapper") +class MybatisplusDemoApplicationTests { + + @Autowired + private EmployeeMapper employeeMapper; + + @Test + void contextLoads() { + // 查询名字中包含'j',年龄大于20岁,邮箱不为空的员工信息 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.like("last_name", "j"); + wrapper.gt("age", 20); + wrapper.isNotNull("email"); + List list = employeeMapper.selectList(wrapper); + list.forEach(System.out::println); + } +} +``` + +运行结果: + +```java +Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0) +``` + +条件构造器提供了丰富的条件方法帮助我们进行条件的构造,比如 `like` 方法会为我们建立模糊查询,查看一下控制台输出的 SQL: + +```sql +==> Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0 AND (last_name LIKE ? AND age > ? AND email IS NOT NULL) +==> Parameters: %j%(String), 20(Integer) +``` + +可以看到它是对 `j` 的前后都加上了 `%` ,若是只想查询以 `j` 开头的名字,则可以使用 `likeRight` 方法,若是想查询以 `j` 结尾的名字,,则使用 `likeLeft` 方法。 + +年龄的比较也是如此, `gt` 是大于指定值,若是小于则调用 `lt` ,大于等于调用 `ge` ,小于等于调用 `le` ,不等于调用 `ne` ,还可以使用 `between` 方法实现这一过程,相关的其它方法都可以查阅源码进行学习。 + +因为这些方法返回的其实都是自身实例,所以可使用链式编程: + +```java +@Test +void contextLoads() { + // 查询名字中包含'j',年龄大于20岁,邮箱不为空的员工信息 + QueryWrapper wrapper = new QueryWrapper() + .likeLeft("last_name", "j") + .gt("age", 20) + .isNotNull("email"); + List list = employeeMapper.selectList(wrapper); + list.forEach(System.out::println); +} +``` + +也可以使用 `LambdaQueryWrapper` 实现: + +```java +@Test +void contextLoads() { + // 查询名字中包含'j',年龄大于20岁,邮箱不为空的员工信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .like(Employee::getLastName,"j") + .gt(Employee::getAge,20) + .isNotNull(Employee::getEmail); + List list = employeeMapper.selectList(wrapper); + list.forEach(System.out::println); +} +``` + +这种方式的好处在于对字段的设置不是硬编码,而是采用方法引用的形式,效果与 `QueryWrapper` 是一样的。 + +`UpdateWrapper` 与 `QueryWrapper` 不同,它的作用是封装更新内容的,比如: + +```java +@Test +void contextLoads() { + UpdateWrapper wrapper = new UpdateWrapper() + .set("age", 50) + .set("email", "emp@163.com") + .like("last_name", "j") + .gt("age", 20); + employeeMapper.update(null, wrapper); +} +``` + +将名字中包含 `j` 且年龄大于 20 岁的员工年龄改为 50,邮箱改为 emp@163.com,`UpdateWrapper` 不仅能够封装更新内容,也能作为查询条件,所以在更新数据时可以直接构造一个 `UpdateWrapper` 来设置更新内容和条件。 \ No newline at end of file From be8de72725a23512861f6c1036cd9716d9fc55a4 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 19 May 2021 11:45:30 +0800 Subject: [PATCH 206/207] Update springboot-mybatis.md --- docs/basis/springboot-mybatis.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/basis/springboot-mybatis.md b/docs/basis/springboot-mybatis.md index 47e6bd4..1955c8c 100644 --- a/docs/basis/springboot-mybatis.md +++ b/docs/basis/springboot-mybatis.md @@ -35,8 +35,6 @@ SpringBoot 整合 Mybatis 有两种常用的方式,一种就是我们常见的 - 数据库:MySQL - SpringBoot版本:2.1.0 - - ### 1.2 创建工程 创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考前面的文章。 From 76c018d7d898d61d9ce07a975c0056a36a5f9358 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 19 May 2021 11:45:32 +0800 Subject: [PATCH 207/207] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 025fee7..e98b843 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ 4. [Spring 如何优雅读取配置文件?](./docs/basis/read-config-properties.md) 5. **异常处理** :[Spring Boot 异常处理的几种方式](./docs/advanced/springboot-handle-exception.md)、[Spring Boot 异常处理在实际项目中的应用](./docs/advanced/springboot-handle-exception-plus.md) 6. **JPA** : [ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md) 、 [JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md) -7. [MyBatis-Plus 从入门到上手干事!](./docs/MyBatisPlus.md) -8. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) -9. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~) +7. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md) +8. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~) +9. [MyBatis-Plus 从入门到上手干事!](./docs/MyBatisPlus.md) 10. [SpringBoot 2.0+ 集成 Swagger 官方 Starter + knife4j 增强方案](./docs/basis/swagger.md) ### 进阶