vault backup: 2026-11-07 16:33:44
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
---
|
||||
page-title: "47个经典Spring面试题详解(附带答案)"
|
||||
url: https://mp.weixin.qq.com/s?__biz=MzU5OTc3NDM2Mw==&mid=2247483928&idx=1&sn=a7dd0674a661402975f7d13a2c3f2095&chksm=feae87ecc9d90efa3a8b642314c9b40ce136201fdfe55534d70e0fc984f3eff861774fa9cb17&scene=27
|
||||
web-message:
|
||||
web-badge-color: ""
|
||||
web-badge-message:
|
||||
date: "2024-03-18 15:55:59"
|
||||
---
|
||||
> **这篇文章主要分享了47个Spring经典面试题,现在分享给大家,也给大家做个参考。正好现在金三银四,希望对大家有所帮助,有用的话点赞支持!**
|
||||
>
|
||||
> **1\. 不同版本的 Spring Framework 有哪些主要功能?**
|
||||
>
|
||||
> **Version**
|
||||
>
|
||||
> **Feature**
|
||||
>
|
||||
> Spring2.5
|
||||
>
|
||||
> 发布于2007年。这是第一-个支持注解的版本。
|
||||
>
|
||||
> Spring3.0
|
||||
>
|
||||
> 发布于2009年。它完全利用了Java5中的改进,并为JEE6提供了支持。
|
||||
>
|
||||
> Spring4.0
|
||||
>
|
||||
> 发布于2013年。这是第一个完全支持JAVA8的版本。
|
||||
>
|
||||
> spring5.0
|
||||
>
|
||||
> 发布于2017年。使用JDK 8特性,支持响应式编程,函数式Web框架,对Kotlin支持。
|
||||
>
|
||||
> **2\. 什么是 Spring Framework?**
|
||||
>
|
||||
> - Spring 是一个开源应用框架,旨在降低应用程序开发的复杂度。
|
||||
>
|
||||
> - 它是轻量级、松散耦合的。
|
||||
>
|
||||
> - 它具有分层体系结构,允许用户选择组件,同时还为 J2EE 应用程序开发提供了一个有凝聚力的框架。
|
||||
>
|
||||
> - 它可以集成其他框架,如 Structs、Hibernate、EJB 等,所以又称为框架的框架。
|
||||
>
|
||||
>
|
||||
> **3. 列举 Spring Framework 的优点。**
|
||||
>
|
||||
> - 由于 Spring Frameworks 的分层架构,用户可以自由选择自己需要的组件。
|
||||
>
|
||||
> - Spring Framework 支持 POJO(Plain Old Java Object) 编程,从而具备持续集成和可测试性。
|
||||
>
|
||||
> - 由于依赖注入和控制反转,JDBC 得以简化。
|
||||
>
|
||||
> - 它是开源免费的。
|
||||
>
|
||||
>
|
||||
> **4\. Spring Framework 有哪些不同的功能?**
|
||||
>
|
||||
> - 轻量级 - Spring 在代码量和透明度方面都很轻便。
|
||||
>
|
||||
> - IOC - 控制反转
|
||||
>
|
||||
> - AOP - 面向切面编程可以将应用业务逻辑和系统服务分离,以实现高内聚。
|
||||
>
|
||||
> - 容器 - Spring 负责创建和管理对象(Bean)的生命周期和配置。
|
||||
>
|
||||
> - MVC - 对 web 应用提供了高度可配置性,其他框架的集成也十分方便。
|
||||
>
|
||||
> - 事务管理 - 提供了用于事务管理的通用抽象层。Spring 的事务支持也可用于容器较少的环境。
|
||||
>
|
||||
> - JDBC 异常 - Spring 的 JDBC 抽象层提供了一个异常层次结构,简化了错误处理策略。
|
||||
>
|
||||
>
|
||||
> **5\. Spring Framework 中有多少个模块,它们分别是什么?**
|
||||
>
|
||||
> 
|
||||
>
|
||||
> Spring 核心容器 – 该层基本上是 Spring Framework 的核心。它包含以下模块:
|
||||
>
|
||||
> - Spring Core
|
||||
>
|
||||
> - Spring Bean
|
||||
>
|
||||
> - SpEL (Spring Expression Language)
|
||||
>
|
||||
> - Spring Context
|
||||
>
|
||||
>
|
||||
> 数据访问/集成 – 该层提供与数据库交互的支持。它包含以下模块:
|
||||
>
|
||||
> - JDBC (Java DataBase Connectivity)
|
||||
>
|
||||
> - ORM (Object Relational Mapping)
|
||||
>
|
||||
> - OXM (Object XML Mappers)
|
||||
>
|
||||
> - JMS (Java Messaging Service)
|
||||
>
|
||||
> - Transaction
|
||||
>
|
||||
>
|
||||
> Web – 该层提供了创建 Web 应用程序的支持。它包含以下模块:AOP – 该层支持面向切面编程
|
||||
>
|
||||
> - Web
|
||||
>
|
||||
> - Web – Servlet
|
||||
>
|
||||
> - Web – Socket
|
||||
>
|
||||
> - Web – Portlet
|
||||
>
|
||||
>
|
||||
> Instrumentation – 该层为类检测和类加载器实现提供支持。
|
||||
>
|
||||
> Test – 该层为使用 JUnit 和 TestNG 进行测试提供支持。
|
||||
>
|
||||
> 几个杂项模块:
|
||||
>
|
||||
> - Messaging – 该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。
|
||||
>
|
||||
> - Aspects – 该模块为与 AspectJ 的集成提供支持。
|
||||
>
|
||||
>
|
||||
> **6. 什么是 Spring 配置文件?**
|
||||
>
|
||||
> Spring 配置文件是 XML 文件。该文件主要包含类信息。它描述了这些类是如何配置以及相互引入的。但是,XML 配置文件冗长且更加干净。如果没有正确规划和编写,那么在大项目中管理变得非常困难。
|
||||
>
|
||||
> **7\. Spring 应用程序有哪些不同组件?**
|
||||
>
|
||||
> Spring 应用一般有以下组件:
|
||||
>
|
||||
> - 接口 - 定义功能。
|
||||
>
|
||||
> - Bean 类 - 它包含属性,setter 和 getter 方法,函数等。
|
||||
>
|
||||
> - Spring 面向切面编程(AOP) - 提供面向切面编程的功能。
|
||||
>
|
||||
> - Bean 配置文件 - 包含类的信息以及如何配置它们。
|
||||
>
|
||||
> - 用户程序 - 它使用接口。
|
||||
>
|
||||
>
|
||||
> **8\. 使用 Spring 有哪些方式?**
|
||||
>
|
||||
> 使用 Spring 有以下方式:
|
||||
>
|
||||
> - 作为作为一个成熟的 Spring Web 应用程序。
|
||||
>
|
||||
> - 作为第三方 Web 框架,使用 Spring Frameworks 中间层。
|
||||
>
|
||||
> - 用于远程使用。
|
||||
>
|
||||
> - 作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects)。Bean,它可以包装现有的 POJO(Plain Old Java Objects)。
|
||||
>
|
||||
>
|
||||
> **9. 什么是 Spring IOC 容器?**
|
||||
>
|
||||
> Spring 框架的核心是 Spring 容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过 XML,Java 注解或 Java 代码提供。
|
||||
>
|
||||
> 
|
||||
>
|
||||
> **10\. 什么是依赖注入?**
|
||||
>
|
||||
> 在依赖注入中,您不必创建对象,但必须描述如何创建它们。您不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC 容器将它们装配在一起。
|
||||
>
|
||||
> **11\. 可以通过多少种方式完成依赖注入?**
|
||||
>
|
||||
> 通常,依赖注入可以通过三种方式完成,即:
|
||||
>
|
||||
> - 构造函数注入
|
||||
>
|
||||
> - setter 注入
|
||||
>
|
||||
> - 接口注入
|
||||
>
|
||||
>
|
||||
> 在 Spring Framework 中,仅使用构造函数和 setter 注入。
|
||||
>
|
||||
> **12\. 区分构造函数注入和 setter 注入**
|
||||
>
|
||||
> **构造函数注入**
|
||||
>
|
||||
> **setter注入**
|
||||
>
|
||||
> 没有部分注入
|
||||
>
|
||||
> 有部分注入
|
||||
>
|
||||
> 不会覆盖setter属性
|
||||
>
|
||||
> 会覆盖setter属性
|
||||
>
|
||||
> 任意修改都会创建一个新实例
|
||||
>
|
||||
> 任意修改不会创建一个新实例
|
||||
>
|
||||
> 适用于设置很多属性
|
||||
>
|
||||
> 适用于设置少量属性
|
||||
>
|
||||
> **13\. spring 中有多少种 IOC 容器?**
|
||||
>
|
||||
> BeanFactory - BeanFactory 就像一个包含 bean 集合的工厂类。它会在客户端要求时实例化 bean。
|
||||
>
|
||||
> ApplicationContext - ApplicationContext 接口扩展了 BeanFactory 接口。它在 BeanFactory 基础上提供了一些额外的功能。
|
||||
>
|
||||
> **14\. 区分 BeanFactory 和 ApplicationContext。**
|
||||
>
|
||||
> **BeanFactory**
|
||||
>
|
||||
> **ApplicationContext**
|
||||
>
|
||||
> 它使用懒加载
|
||||
>
|
||||
> 它使用即时加载
|
||||
>
|
||||
> 它使用语法显式提供资源对象
|
||||
>
|
||||
> 它自己创建和管理资源对象
|
||||
>
|
||||
> 不支持国际化
|
||||
>
|
||||
> 支持国际化
|
||||
>
|
||||
> 不支持基于依赖的注解
|
||||
>
|
||||
> 支持基于依赖的注解
|
||||
>
|
||||
> **15\. 列举 IoC 的一些好处。**
|
||||
>
|
||||
> IoC 的一些好处是:
|
||||
>
|
||||
> - 它将最小化应用程序中的代码量。
|
||||
>
|
||||
> - 它将使您的应用程序易于测试,因为它不需要单元测试用例中的任何单例或 JNDI 查找机制。
|
||||
>
|
||||
> - 它以最小的影响和最少的侵入机制促进松耦合。
|
||||
>
|
||||
> - 它支持即时的实例化和延迟加载服务。
|
||||
>
|
||||
>
|
||||
> **16\. Spring IoC 的实现机制。**
|
||||
>
|
||||
> Spring 中的 IoC 的实现原理就是工厂模式加反射机制。
|
||||
>
|
||||
> 示例:
|
||||
>
|
||||
> ```
|
||||
> interface Fruit {
|
||||
> ```
|
||||
>
|
||||
> **17\. 什么是 spring bean?**
|
||||
>
|
||||
> - 它们是构成用户应用程序主干的对象。
|
||||
>
|
||||
> - Bean 由 Spring IoC 容器管理。
|
||||
>
|
||||
> - 它们由 Spring IoC 容器实例化,配置,装配和管理。
|
||||
>
|
||||
> - Bean 是基于用户提供给容器的配置元数据创建。
|
||||
>
|
||||
>
|
||||
> **18\. spring 提供了哪些配置方式?**
|
||||
>
|
||||
> 基于 xml 配置
|
||||
>
|
||||
> bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如:
|
||||
>
|
||||
> ```
|
||||
> <bean id="studentbean" class="org.edureka.firstSpring.StudentBean">
|
||||
> ```
|
||||
>
|
||||
> 基于注解配置
|
||||
>
|
||||
> 您可以通过在相关的类,方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如:
|
||||
>
|
||||
> ```
|
||||
> <beans>
|
||||
> ```
|
||||
>
|
||||
> 基于 Java API 配置
|
||||
>
|
||||
> Spring 的 Java 配置是通过使用@Bean 和 @Configuration 来实现。
|
||||
>
|
||||
> - @Bean 注解扮演与 <bean /> 元素相同的角色。
|
||||
>
|
||||
> - @Configuration 类允许通过简单地调用同一个类中的其他@Bean 方法来定义 bean 间依赖关系。
|
||||
>
|
||||
>
|
||||
> 例如:
|
||||
>
|
||||
> ```
|
||||
> @Bean
|
||||
> ```
|
||||
>
|
||||
> **19\. spring 支持集中 bean scope?**
|
||||
>
|
||||
> Spring bean 支持 5 种 scope:
|
||||
>
|
||||
> - Singleton - 每个 Spring IoC 容器仅有一个单实例。
|
||||
>
|
||||
> - Prototype - 每次请求都会产生一个新的实例。
|
||||
>
|
||||
> - Request - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。
|
||||
>
|
||||
> - Session - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。
|
||||
>
|
||||
> - Global-session - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。
|
||||
>
|
||||
>
|
||||
> 仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。
|
||||
>
|
||||
> **20\. spring bean 容器的生命周期是什么样的?**
|
||||
>
|
||||
> spring bean 容器的生命周期流程如下:
|
||||
>
|
||||
> - Spring 容器根据配置中的 bean 定义中实例化 bean。
|
||||
>
|
||||
> - Spring 使用依赖注入填充所有属性,如 bean 中所定义的配置。
|
||||
>
|
||||
> - 如果 bean 实现 BeanNameAware 接口,则工厂通过传递 bean 的 ID 来调用 setBeanName()。
|
||||
>
|
||||
> - 如果 bean 实现 BeanFactoryAware 接口,工厂通过传递自身的实例来调用 setBeanFactory()。
|
||||
>
|
||||
> - 如果存在与 bean 关联的任何 BeanPostProcessors,则调用 preProcessBeforeInitialization() 方法。
|
||||
>
|
||||
> - 如果为 bean 指定了 init 方法( <bean> 的 init-method 属性),那么将调用它。
|
||||
>
|
||||
> - 最后,如果存在与 bean 关联的任何 BeanPostProcessors,则将调用 postProcessAfterInitialization() 方法。
|
||||
>
|
||||
> - 如果 bean 实现 DisposableBean 接口,当 spring 容器关闭时,会调用 destory()。
|
||||
>
|
||||
> - 如果为 bean 指定了 destroy 方法( <bean> 的 destroy-method 属性),那么将调用它。
|
||||
>
|
||||
>
|
||||
> 
|
||||
>
|
||||
> **21\. 什么是 spring 的内部 bean?**
|
||||
>
|
||||
> 只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 <property> 或 <constructor-arg> 中提供了 <bean> 元素的使用。内部 bean 总是匿名的,它们总是作为原型。
|
||||
>
|
||||
> 例如,假设我们有一个 Student 类,其中引用了 Person 类。这里我们将只创建一个 Person 类实例并在 Student 中使用它。
|
||||
>
|
||||
> Student.java
|
||||
>
|
||||
> ```
|
||||
> public class Student {
|
||||
> ```
|
||||
>
|
||||
> bean.xml
|
||||
>
|
||||
> ```
|
||||
> <bean id=“StudentBean" class="com.edureka.Student">
|
||||
> ```
|
||||
>
|
||||
> **22\. 什么是 spring 装配**
|
||||
>
|
||||
> 当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。
|
||||
>
|
||||
> **23\. 自动装配有哪些方式?**
|
||||
>
|
||||
> Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。
|
||||
>
|
||||
> 自动装配的不同模式:
|
||||
>
|
||||
> - no - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。
|
||||
>
|
||||
> - byName - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。
|
||||
>
|
||||
> - byType - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。
|
||||
>
|
||||
> - 构造函数 - 它通过调用类的构造函数来注入依赖项。它有大量的参数。
|
||||
>
|
||||
> - autodetect - 首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。
|
||||
>
|
||||
>
|
||||
> **24\. 自动装配有什么局限?**
|
||||
>
|
||||
> - 覆盖的可能性 - 您始终可以使用 <constructor-arg> 和 <property> 设置指定依赖项,这将覆盖自动装配。
|
||||
>
|
||||
> - 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。
|
||||
>
|
||||
> - 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。
|
||||
>
|
||||
>
|
||||
> **25. 你用过哪些重要的 Spring 注解?**
|
||||
>
|
||||
> - @Controller - 用于 Spring MVC 项目中的控制器类。
|
||||
>
|
||||
> - @Service - 用于服务类。
|
||||
>
|
||||
> - @RequestMapping - 用于在控制器处理程序方法中配置 URI 映射。
|
||||
>
|
||||
> - @ResponseBody - 用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。
|
||||
>
|
||||
> - @PathVariable - 用于将动态值从 URI 映射到处理程序方法参数。
|
||||
>
|
||||
> - @Autowired - 用于在 spring bean 中自动装配依赖项。
|
||||
>
|
||||
> - @Qualifier - 使用 @Autowired 注解,以避免在存在多个 bean 类型实例时出现混淆。
|
||||
>
|
||||
> - @Scope - 用于配置 spring bean 的范围。
|
||||
>
|
||||
> - @Configuration,@ComponentScan 和 @Bean - 用于基于 java 的配置。
|
||||
>
|
||||
> - @Aspect,@Before,@After,@Around,@Pointcut - 用于切面编程(AOP)。
|
||||
>
|
||||
>
|
||||
> **26\. 如何在 spring 中启动注解装配?**
|
||||
>
|
||||
> 默认情况下,Spring 容器中未打开注解装配。因此,要使用基于注解装配,我们必须通过配置 <context:annotation-config /> 元素在 Spring 配置文件中启用它。
|
||||
>
|
||||
> **27\. @Component, @Controller, @Repository, @Service 有何区别?**
|
||||
>
|
||||
> - @Component:这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。
|
||||
>
|
||||
> - @Controller:这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。
|
||||
>
|
||||
> - @Service:此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component,因为它以更好的方式指定了意图。
|
||||
>
|
||||
> - @Repository:这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。
|
||||
>
|
||||
>
|
||||
> **28. @Required 注解有什么用?**
|
||||
>
|
||||
> @Required 应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用 bean 定义中的显式属性值或使用自动装配填充受影响的 bean 属性。如果尚未填充受影响的 bean 属性,则容器将抛出 BeanInitializationException。
|
||||
>
|
||||
> 示例:
|
||||
>
|
||||
> ```
|
||||
> public class Employee {
|
||||
> ```
|
||||
>
|
||||
> **29.@Autowired 注解有什么用?**
|
||||
>
|
||||
> @Autowired 可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在 setter 方法,构造函数,具有任意名称或多个参数的属性或方法上自动装配 bean。默认情况下,它是类型驱动的注入。
|
||||
>
|
||||
> ```
|
||||
> public class Employee {
|
||||
> ```
|
||||
>
|
||||
> **30. **@Qualifier 注解有什么用?****
|
||||
>
|
||||
> 当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。
|
||||
>
|
||||
> 例如,这里我们分别有两个类,Employee 和 EmpAccount。在 EmpAccount 中,使用@Qualifier 指定了必须装配 id 为 emp1 的 bean。
|
||||
>
|
||||
> Employee.java
|
||||
>
|
||||
> ```
|
||||
> public class Employee {
|
||||
> ```
|
||||
>
|
||||
> EmpAccount.java
|
||||
>
|
||||
> ```
|
||||
> public class EmpAccount {
|
||||
> ```
|
||||
>
|
||||
> **31.@RequestMapping 注解有什么用?**
|
||||
>
|
||||
> @RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注解可应用于两个级别:
|
||||
>
|
||||
> - 类级别:映射请求的 URL
|
||||
>
|
||||
> - 方法级别:映射 URL 以及 HTTP 请求方法
|
||||
>
|
||||
>
|
||||
> **32\. spring DAO 有什么用?**
|
||||
>
|
||||
> Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。
|
||||
>
|
||||
> **33\. 列举 Spring DAO 抛出的异常。**
|
||||
>
|
||||
> 
|
||||
>
|
||||
> **34\. spring JDBC API 中存在哪些类?**
|
||||
>
|
||||
> - JdbcTemplate
|
||||
>
|
||||
> - SimpleJdbcTemplate
|
||||
>
|
||||
> - NamedParameterJdbcTemplate
|
||||
>
|
||||
> - SimpleJdbcInsert
|
||||
>
|
||||
> - SimpleJdbcCall
|
||||
>
|
||||
>
|
||||
> **35\. 使用 Spring 访问 Hibernate 的方法有哪些?**
|
||||
>
|
||||
> 我们可以通过两种方式使用 Spring 访问 Hibernate:
|
||||
>
|
||||
> - 使用 Hibernate 模板和回调进行控制反转
|
||||
>
|
||||
> - 扩展 HibernateDAOSupport 并应用 AOP 拦截器节点
|
||||
>
|
||||
>
|
||||
> **36\. 列举 spring 支持的事务管理类型**
|
||||
>
|
||||
> Spring 支持两种类型的事务管理:
|
||||
>
|
||||
> - 程序化事务管理:在此过程中,在编程的帮助下管理事务。它为您提供极大的灵活性,但维护起来非常困难。
|
||||
>
|
||||
> - 声明式事务管理:在此,事务管理与业务代码分离。仅使用注解或基于 XML 的配置来管理事务。
|
||||
>
|
||||
>
|
||||
> spring 支持哪些 ORM 框架
|
||||
>
|
||||
> - Hibernate
|
||||
>
|
||||
> - iBatis
|
||||
>
|
||||
> - JPA
|
||||
>
|
||||
> - JDO
|
||||
>
|
||||
> - OJB
|
||||
>
|
||||
>
|
||||
> **37.什么是 AOP?**
|
||||
>
|
||||
> AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)
|
||||
>
|
||||
> **38\. AOP 中的 Aspect、Advice、Pointcut、JointPoint 和 Advice 参数分别是什么?**
|
||||
>
|
||||
> 
|
||||
>
|
||||
> - Aspect - Aspect 是一个实现交叉问题的类,例如事务管理。方面可以是配置的普通类,然后在 Spring Bean 配置文件中配置,或者我们可以使用 Spring AspectJ 支持使用 @Aspect 注解将类声明为 Aspect。
|
||||
>
|
||||
> - Advice - Advice 是针对特定 JoinPoint 采取的操作。在编程方面,它们是在应用程序中达到具有匹配切入点的特定 JoinPoint 时执行的方法。您可以将 Advice 视为 Spring 拦截器(Interceptor)或 Servlet 过滤器(filter)。
|
||||
>
|
||||
> - Advice Arguments - 我们可以在 advice 方法中传递参数。我们可以在切入点中使用 args() 表达式来应用于与参数模式匹配的任何方法。如果我们使用它,那么我们需要在确定参数类型的 advice 方法中使用相同的名称。
|
||||
>
|
||||
> - Pointcut - Pointcut 是与 JoinPoint 匹配的正则表达式,用于确定是否需要执行 Advice。 Pointcut 使用与 JoinPoint 匹配的不同类型的表达式。Spring 框架使用 AspectJ Pointcut 表达式语言来确定将应用通知方法的 JoinPoint。
|
||||
>
|
||||
> - JoinPoint - JoinPoint 是应用程序中的特定点,例如方法执行,异常处理,更改对象变量值等。在 Spring AOP 中,JoinPoint 始终是方法的执行器。
|
||||
>
|
||||
>
|
||||
> **39\. 什么是通知(Advice)?**
|
||||
>
|
||||
> 特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。
|
||||
>
|
||||
> **40\. 有哪些类型的通知(Advice)?**
|
||||
>
|
||||
> - Before - 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。
|
||||
>
|
||||
> - After Returning - 这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
|
||||
>
|
||||
> - After Throwing - 这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。
|
||||
>
|
||||
> - After (finally) - 这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
|
||||
>
|
||||
> - Around - 这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。
|
||||
>
|
||||
>
|
||||
> **41\. 指出在 spring aop 中 concern 和 cross-cutting concern 的不同之处。**
|
||||
>
|
||||
> concern 是我们想要在应用程序的特定模块中定义的行为。它可以定义为我们想要实现的功能。
|
||||
>
|
||||
> cross-cutting concern 是一个适用于整个应用的行为,这会影响整个应用程序。例如,日志记录,安全性和数据传输是应用程序几乎每个模块都需要关注的问题,因此它们是跨领域的问题。
|
||||
>
|
||||
> **41\. AOP 有哪些实现方式?**
|
||||
>
|
||||
> 实现 AOP 的技术,主要分为两大类:
|
||||
>
|
||||
> - 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;
|
||||
>
|
||||
>
|
||||
> - 编译时编织(特殊编译器实现)
|
||||
>
|
||||
> - 类加载时编织(特殊的类加载器实现)。
|
||||
>
|
||||
>
|
||||
> - 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
|
||||
>
|
||||
>
|
||||
> - JDK 动态代理
|
||||
>
|
||||
> - CGLIB
|
||||
>
|
||||
>
|
||||
> **42\. Spring AOP and AspectJ AOP 有什么区别?**
|
||||
>
|
||||
> Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。
|
||||
>
|
||||
> **43\. 如何理解 Spring 中的代理?**
|
||||
>
|
||||
> 将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的
|
||||
>
|
||||
> ```
|
||||
> `Advice + Target Object = Proxy`
|
||||
> ```
|
||||
>
|
||||
> **44. 什么是编织(Weaving)?**
|
||||
>
|
||||
> 为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。请参考下图:
|
||||
>
|
||||
> 
|
||||
>
|
||||
> **45\. Spring MVC 框架有什么用?**
|
||||
>
|
||||
> Spring Web MVC 框架提供 模型-视图-控制器 架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。 MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。
|
||||
>
|
||||
> **46\. 描述一下 DispatcherServlet 的工作流程**
|
||||
>
|
||||
> DispatcherServlet 的工作流程可以用一幅图来说明:
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 1\. 向服务器发送 HTTP 请求,请求被前端控制器 DispatcherServlet 捕获。
|
||||
>
|
||||
> 2. DispatcherServlet 根据 -servlet.xml 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返回。
|
||||
>
|
||||
> 3. DispatcherServlet 根据获得的 Handler ,选择一个合适的 HandlerAdapter 。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 preHandler(...)方法)。
|
||||
>
|
||||
> 4\. 提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler ( Controller )。 在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:Handler(Controller)执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象;
|
||||
>
|
||||
> - HttpMessageConveter : 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。
|
||||
>
|
||||
> - 数据转换 :对请求消息进行数据转换。如 String 转换成 Integer 、 Double 等。
|
||||
>
|
||||
> - 数据根式化 :对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。
|
||||
>
|
||||
> - 数据验证 : 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中。
|
||||
>
|
||||
>
|
||||
> 5\. 根据返回的 ModelAndView ,选择一个适合的 ViewResolver (必须是已经注册到 Spring 容器中的 ViewResolver )返回给 DispatcherServlet 。
|
||||
>
|
||||
> 6. ViewResolver 结合 Model 和 View ,来渲染视图。
|
||||
>
|
||||
> 7\. 视图负责将渲染结果返回给客户端。
|
||||
>
|
||||
> **47\. 介绍一下 WebApplicationContext**
|
||||
>
|
||||
> WebApplicationContext 是 ApplicationContext 的扩展。它具有 Web 应用程序所需的一些额外功能。它与普通的 ApplicationContext 在解析主题和决定与哪个 servlet 关联的能力方面有所不同。
|
||||
>
|
||||
> 
|
||||
@@ -0,0 +1,25 @@
|
||||
## Enum 转 int
|
||||
|
||||
`C.ordinal()`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## int 转 Enum
|
||||
|
||||
只能遍历
|
||||
|
||||
```Plain Text
|
||||
for (MyEnum type : values()) {
|
||||
if (type.getId() == id) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
|
||||
## 一、简介
|
||||
|
||||
Java Cryptography Extension(JCE)是一组包,它们提供用于加密、[密钥](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fbaike.baidu.com%2Fview%2F934.htm&source=article&objectId=2040606)生成和协商以及 Message Authentication Code(MAC)算法的框架和实现。它提供对对称、不对称、块和流密码的加密支持,它还支持安全流和密封的对象。它不对外出口,用它开发完成封装后将无法调用。
|
||||
|
||||
在早期JDK版本中,由于受美国的密码出口条例约束,Java中涉及加解密功能的API被限制出口,所以Java中安全组件被分成了两部分: 不含加密功能的JCA(Java Cryptography Architecture )和含加密功能的JCE(Java Cryptography Extension)。
|
||||
|
||||
在JDK1.1-1.3版本期间,JCE属于扩展包,仅供美国和加拿大的用户下载,JDK1.4+版本后,随JDK核心包一起分发。
|
||||
|
||||
- [Java加密体系结构(JCA)参考指南](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fdocs.oracle.com%2Fjavase%2F7%2Fdocs%2Ftechnotes%2Fguides%2Fsecurity%2Fcrypto%2FCryptoSpec.html&source=article&objectId=2040606)
|
||||
- [**Java密体系结构标准算法的名字的文档**](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fdocs.oracle.com%2Fjavase%2F7%2Fdocs%2Ftechnotes%2Fguides%2Fsecurity%2FStandardNames.html&source=article&objectId=2040606)
|
||||
|
||||
## 二、加解密
|
||||
|
||||
JCE的API都在 javax.crypto 包下,核心功能包括:加解密、密钥生成(对称)、MAC生成、密钥协商。
|
||||
|
||||
### 1. Cipher
|
||||
|
||||
加解密功能由Cipher组件提供,其也是JCE中最核心的组件。 Cipher的几个知识点:
|
||||
|
||||
1. Cipher在使用时需以参数方式指定transformation
|
||||
2. transformation的格式为algorithm/mode/padding(_算法/模式/填充_),其中algorithm(算法)为必输项,如: DES/CBC/PKCS5Padding
|
||||
3. 缺省的mode为ECB,缺省的padding为PKCS5Padding
|
||||
4. 在block算法与流加密模式组合时, 需在mode后面指定每次处理的bit数, 如DES/CFB8/NoPadding, 如未指定则使用缺省值, SunJCE缺省值为64bits
|
||||
5. Cipher有4种操作模式: ENCRYPT_MODE(加密), DECRYPT_MODE(解密), WRAP_MODE(导出Key), UNWRAP_MODE(导入Key),初始化时需指定某种操作模式(都是静态参数)。
|
||||
|
||||
### 2. 对称加密的算法与密钥长度选择
|
||||
|
||||
| 算法名称 | 密钥长 | 块长 | 速度 | 说明 |
|
||||
| -------- | ------------- | --- | --- | -------------------- |
|
||||
| DES | 56 | 64 | 慢 | 不安全, 不要使用 |
|
||||
| 3DES | 112/168 | 64 | 很慢 | 中等安全, 适合加密较小的数据 |
|
||||
| AES | 128, 192, 256 | 128 | 快 | 安全 |
|
||||
| Blowfish | (4至56)*8 | 64 | 快 | 应该安全, 在安全界尚未被充分分析、论证 |
|
||||
| RC4 | 40-1024 | 64 | 很快 | 安全性不明确 |
|
||||
|
||||
一般情况下,不要选择DES算法,推荐使用AES算法。一般认为128bits的密钥已足够安全,如果可以请选择256bits的密钥。
|
||||
|
||||
<1>密钥长度是在生成密钥时指定的。如:
|
||||
|
||||
```javascript
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES/CBC/PKCS5PADDING");
|
||||
generator.init(256);
|
||||
SecretKey key = generator.generateKey();
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
<2>生成长度超128bits的密钥,需单独从Oracle官网下载对应JDK版本的Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files文件《详见: [java加密——Jasypt开源工具包](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fblog.csdn.net%2Ffanxiaobin577328725%2Farticle%2Fdetails%2F51707882&source=article&objectId=2040606)》,例如[JDK7对应的jurisdiction policy files。](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fwww.oracle.com%2Ftechnetwork%2Fjava%2Fjavase%2Fdownloads%2Fjce-7-download-432124.html&source=article&objectId=2040606)
|
||||
|
||||
### 3.加密示例代码
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 根据密钥{@link #getKey()}对指定的明文
|
||||
plainText
|
||||
进行加密.
|
||||
*
|
||||
* @param plainText 明文
|
||||
* @return 加密后的密文.
|
||||
*/
|
||||
public static final String encrypt(String plainText) {
|
||||
Key secretKey = getKey();
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] p = plainText.getBytes("UTF-8");
|
||||
byte[] result = cipher.doFinal(p);
|
||||
BASE64Encoder encoder = new BASE64Encoder();
|
||||
String encoded = encoder.encode(result);
|
||||
return encoded;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
### 4.解密示例代码
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 根据密钥{@link #getKey()}对指定的密文
|
||||
cipherText
|
||||
进行解密.
|
||||
*
|
||||
* @param cipherText 密文
|
||||
* @return 解密后的明文.
|
||||
*/
|
||||
public static final String decrypt(String cipherText) {
|
||||
Key secretKey = getKey();
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
BASE64Decoder decoder = new BASE64Decoder();
|
||||
byte[] c = decoder.decodeBuffer(cipherText);
|
||||
byte[] result = cipher.doFinal(c);
|
||||
String plainText = new String(result, "UTF-8");
|
||||
return plainText;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
### 5.带算法参数的加解密
|
||||
|
||||
Cipher可能用到算法参数(AlgorithmParameterSpec或AlgorithmParameters)的情形:
|
||||
|
||||
- a. DES, DES-EDE, and Blowfish使用feedback模式时(如CBC, CFB, OFB或PCBC), 将用到IV
|
||||
- b. PBEWithMD5AndDES将用到salt和iteration count
|
||||
|
||||
下面是采用PBE算法进行加解密的示例:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 提供基于口令的加密功能.
|
||||
*
|
||||
* @param plainText 明文
|
||||
* @return 加密后的密文.
|
||||
*/
|
||||
public static final String pbeEncrypt(String plainText) {
|
||||
Key pbeSecretKey = getPBEKey();
|
||||
PBEParameterSpec pbeParamSpec = getParamSpec();
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, pbeSecretKey, pbeParamSpec);
|
||||
byte[] p = plainText.getBytes("UTF-8");
|
||||
byte[] result = cipher.doFinal(p);
|
||||
BASE64Encoder encoder = new BASE64Encoder();
|
||||
String encoded = encoder.encode(result);
|
||||
return encoded;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供基于口令的解密功能.
|
||||
*
|
||||
* @param cipherText 密文
|
||||
* @return 解密后的明文.
|
||||
*/
|
||||
public static final String pbeDecrypt(String cipherText) {
|
||||
Key pbeSecretKey = getPBEKey();
|
||||
PBEParameterSpec pbeParamSpec = getParamSpec();
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, pbeSecretKey, pbeParamSpec);
|
||||
BASE64Decoder decoder = new BASE64Decoder();
|
||||
byte[] c = decoder.decodeBuffer(cipherText);
|
||||
byte[] result = cipher.doFinal(c);
|
||||
String plainText = new String(result, "UTF-8");
|
||||
return plainText;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取PBE算法的密钥. 注意PBE密钥由用户提供的口令构造出来的,
|
||||
* 用户提供的口令务必使用char数组, 而不能使用字符串, 字符数
|
||||
* 组用完即清空.
|
||||
*
|
||||
* @return PBE算法的密钥.
|
||||
*/
|
||||
private static final Key getPBEKey() {
|
||||
// TODO come from db or System.in, NOTE: MUST be char array, not java.lang.String
|
||||
char[] pwd = {'%', '_', 'A', 's', '9', 'K'};
|
||||
SecretKey pbeKey = null;
|
||||
PBEKeySpec pbeKeySpec = new PBEKeySpec(pwd);
|
||||
try {
|
||||
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
|
||||
pbeKey = keyFac.generateSecret(pbeKeySpec);
|
||||
return pbeKey;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
Arrays.fill(pwd, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取PBE的算法参数, 涉及salt和iterate count两个参数.
|
||||
*
|
||||
* @return PBE的算法参数.
|
||||
*/
|
||||
private static final PBEParameterSpec getParamSpec() {
|
||||
byte[] salt = { (byte) 0xab, (byte) 0x58, (byte) 0xa1, (byte) 0x8c,
|
||||
(byte) 0x3e, (byte) 0xc8, (byte) 0x9d, (byte) 0x7a };
|
||||
int count = 20;
|
||||
PBEParameterSpec paramSpec = new PBEParameterSpec(salt, count);
|
||||
return paramSpec;
|
||||
}
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
测试代码:
|
||||
|
||||
```javascript
|
||||
String pbePlainText = "127Kjk$%2^";
|
||||
String pbeCipherText = pbeEncrypt(pbePlainText);
|
||||
String pbePlainText2 = pbeDecrypt(pbeCipherText);
|
||||
if (!pbePlainText.equals(pbePlainText2)) {
|
||||
System.out.println("PBE Something wrong");
|
||||
}
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
## 三、密钥生成
|
||||
|
||||
非对称密钥的生成请参考java.security.KeyPairGenerator,样例代码请参考[JCA](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fjloser.com%2Fjava%25E5%25AE%2589%25E5%2585%25A8jca%25E7%25BB%2584%25E4%25BB%25B6%2F&source=article&objectId=2040606)中的示例,对称密钥生成的示例代码如下
|
||||
|
||||
```javascript
|
||||
KeyGenerator gen = KeyGenerator.getInstance("DES");
|
||||
gen.init(56, new SecureRandom());
|
||||
Key key= gen.generateKey();
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
## 四、MAC生成
|
||||
|
||||
MAC技术用于确认数据的完整性,Mac要求通讯双方共享一个secret key,示例代码如下:
|
||||
|
||||
```javascript
|
||||
Key key = KeyGeneratorDemo.generateMac();
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(key);
|
||||
String msg = "新庄杨渡10#";
|
||||
byte[] result = mac.doFinal(msg.getBytes("UTF-8"));
|
||||
BASE64Encoder encoder = new BASE64Encoder();
|
||||
System.out.println(encoder.encode(result));
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
MAC优于数据摘要的地方在于:MAC双方要共享一个密钥,所以其也有互相认证的功能,可有效防止数据摘要中明文和数据摘要被同时替换而无法发现的问题。
|
||||
|
||||
## 五、密钥协商
|
||||
|
||||
密钥协商就是在通讯多方间不直接交换通讯密钥的情况下而选择一个大家达成一致的密钥(session key),这个session key是对称密钥。
|
||||
|
||||
### 1. 密钥协商可以通过2种途径实现
|
||||
|
||||
1. 通过KeyAgreement组件完成,常用算法包括DH(Diffie-Hellman),ECDH(Elliptic Curve Diffie-Hellman),ECMQV(Elliptic Curve Menezes-Qu-Vanstone)等。
|
||||
2. 通过数字信封完成,常用算法包括RSA等。
|
||||
|
||||
### 2. 通过KeyAgreement使用DH算法协商密钥
|
||||
|
||||
a. DH算法由PKCS#3定义,DH算法需在多方间交换公钥,大素数p,私钥的基数g,和私钥的长度l。设协商密钥的双方为Alice和Bob,则协商共涉及5个阶段:
|
||||
|
||||
- i. Alice生成DH公私密钥对
|
||||
- ii. Alice将公钥和算法参数p,g和l发送给Bob
|
||||
- iii. Bob根据算法参数生成自己的公私密钥对,并将公钥发送给Alice
|
||||
- iv. Alice使用自己的私钥和Bob的公钥通过KeyAgreement得到通讯密钥
|
||||
- v. Bob使用自己的私钥和Alice的公钥通过KeyAgreement得到与Alice相同的通讯密钥
|
||||
|
||||
b. 下面的代码演示了使用DH算法实现密钥协商,设通讯一方为Alice,另一方为Bob,分别由两个线程模拟,实际数据交换由共享内存模拟:
|
||||
|
||||
```javascript
|
||||
/*
|
||||
* @(#)KeyAgreementDemo.java 1.0 2012-4-24
|
||||
*
|
||||
* Copyright 2010 Richard Chen(utopia_rabbi@sse.buaa.edu.cn) All Rights Reserved.
|
||||
* PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
||||
*/
|
||||
package charpter.security.keyagreement;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.interfaces.DHPublicKey;
|
||||
import javax.crypto.spec.DESKeySpec;
|
||||
import javax.crypto.spec.DHParameterSpec;
|
||||
|
||||
/**
|
||||
* 演示Deffie-Hellman密钥交换组件的用法.
|
||||
*
|
||||
* 密钥交换就是在通讯多方间不直接交换通讯密钥的情况下而选择一个大家达成一致的密钥.
|
||||
*
|
||||
* @author Rich, 2012-4-24.
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
public class KeyAgreementDemo implements Runnable {
|
||||
/** bob和alice的公钥内容. */
|
||||
byte bob[], alice[];
|
||||
/** alice侧是交换的发起方, 是否已启动. */
|
||||
boolean doneAlice = false;
|
||||
/** 使用计算出来的交换密钥加密过的报文. */
|
||||
byte[] ciphertext;
|
||||
/** DH算法涉及的算法参数, alice侧的大素数p, alice侧的私钥基数g. */
|
||||
BigInteger aliceP, aliceG;
|
||||
/** DH算法涉及的算法参数, alice的私钥长度l. */
|
||||
int aliceL;
|
||||
|
||||
public synchronized void run() {
|
||||
if (!doneAlice) {
|
||||
doAlice();
|
||||
doneAlice = true;
|
||||
} else
|
||||
doBob();
|
||||
}
|
||||
|
||||
public synchronized void doAlice() {
|
||||
try {
|
||||
// Step 1: Alice generates a key pair
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DH");
|
||||
kpg.initialize(1024);
|
||||
KeyPair kp = kpg.generateKeyPair();
|
||||
// Step 2: Alice sends the public key and the
|
||||
// Diffie-Hellman key parameters to Bob
|
||||
Class dhClass = Class.forName("javax.crypto.spec.DHParameterSpec");
|
||||
DHParameterSpec dhSpec = ((DHPublicKey) kp.getPublic()).getParams();
|
||||
aliceG = dhSpec.getG();
|
||||
aliceP = dhSpec.getP();
|
||||
aliceL = dhSpec.getL();
|
||||
alice = kp.getPublic().getEncoded();
|
||||
notify();
|
||||
// Step 4 part 1: Alice performs the first phase of the
|
||||
// protocol with her private key
|
||||
KeyAgreement ka = KeyAgreement.getInstance("DH");
|
||||
ka.init(kp.getPrivate());
|
||||
// Step 4 part 2: Alice performs the second phase of the
|
||||
// protocol with Bob's public key
|
||||
while (bob == null) {
|
||||
wait();
|
||||
}
|
||||
KeyFactory kf = KeyFactory.getInstance("DH");
|
||||
X509EncodedKeySpec x509Spec = new X509EncodedKeySpec(bob);
|
||||
PublicKey pk = kf.generatePublic(x509Spec);
|
||||
ka.doPhase(pk, true);
|
||||
// Step 4 part 3: Alice can generate the secret key
|
||||
byte secret[] = ka.generateSecret();
|
||||
// Step 6: Alice converts a secret key
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("DES");
|
||||
DESKeySpec desSpec = new DESKeySpec(secret);
|
||||
SecretKey key = skf.generateSecret(desSpec);
|
||||
// Step 7: Alice encrypts data with the key and sends
|
||||
// the encrypted data to Bob
|
||||
Cipher c = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
||||
c.init(Cipher.ENCRYPT_MODE, key);
|
||||
ciphertext = c.doFinal("Stand and unfold yourself".getBytes());
|
||||
notify();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void doBob() {
|
||||
try {
|
||||
// Step 3: Bob uses the parameters supplied by Alice
|
||||
// to generate a key pair and sends the public key
|
||||
while (alice == null) {
|
||||
wait();
|
||||
}
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DH");
|
||||
DHParameterSpec dhSpec = new DHParameterSpec(aliceP, aliceG, aliceL);
|
||||
kpg.initialize(dhSpec);
|
||||
KeyPair kp = kpg.generateKeyPair();
|
||||
bob = kp.getPublic().getEncoded();
|
||||
notify();
|
||||
// Step 5 part 1: Bob uses his private key to perform the
|
||||
// first phase of the protocol
|
||||
KeyAgreement ka = KeyAgreement.getInstance("DH");
|
||||
ka.init(kp.getPrivate());
|
||||
// Step 5 part 2: Bob uses Alice's public key to perform
|
||||
// the second phase of the protocol.
|
||||
KeyFactory kf = KeyFactory.getInstance("DH");
|
||||
X509EncodedKeySpec x509Spec = new X509EncodedKeySpec(alice);
|
||||
PublicKey pk = kf.generatePublic(x509Spec);
|
||||
ka.doPhase(pk, true);
|
||||
// Step 5 part 3: Bob generates the secret key
|
||||
byte secret[] = ka.generateSecret();
|
||||
// Step 6: Bob generates a DES key
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("DES");
|
||||
DESKeySpec desSpec = new DESKeySpec(secret);
|
||||
SecretKey key = skf.generateSecret(desSpec);
|
||||
// Step 8: Bob receives the encrypted text and decrypts it
|
||||
Cipher c = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
||||
c.init(Cipher.DECRYPT_MODE, key);
|
||||
while (ciphertext == null) {
|
||||
wait();
|
||||
}
|
||||
byte plaintext[] = c.doFinal(ciphertext);
|
||||
System.out.println("Bob got the string " + new String(plaintext));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
KeyAgreementDemo test = new KeyAgreementDemo();
|
||||
new Thread(test).start(); // Starts Alice
|
||||
new Thread(test).start(); // Starts Bob
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
以上代码参考了Java Cryptography Extension (JCE) Reference Guide(JDK5.0)中Appendix F的例子。
|
||||
|
||||
### 3. 通过数字信封使用RSA算法协商密钥
|
||||
|
||||
数字信封的原理就是利用通讯对方的公钥加密目标密钥(session key,对称密钥),使用目标密钥对报文进行加密,然后将密钥密文与报文密文一起发送给接收方。接收方首先使用自己的私钥对密钥报文进行解密,这样就得到了协商后的密钥,再使用解密后的密钥解密报文,这样就得到了业务数据。过程图示如下:
|
||||
|
||||
代码示例如下,密钥协商双方由两个线程模拟:
|
||||
|
||||
```javascript
|
||||
/*
|
||||
* @(#)DigitalEnvelopeDemo.java 1.0 2012-6-14
|
||||
*
|
||||
* Copyright 2010 Richard Chen(utopia_rabbi@sse.buaa.edu.cn) All Rights Reserved.
|
||||
* PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
||||
*/
|
||||
package charpter.security.keyagreement;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.DESKeySpec;
|
||||
|
||||
import charpter.security.key.KeyGeneratorDemo;
|
||||
|
||||
/**
|
||||
* 演示使用Digital envelope技术进行密钥交换.
|
||||
*
|
||||
|
||||
|
||||
* 服务器端与客户端分别由两个独立的线程模拟, 双方指间的通讯由共享内存实现.
|
||||
*
|
||||
* @author Rich, 2012-6-14.
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class DigitalEnvelopeDemo {
|
||||
|
||||
/**
|
||||
* @param args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
KeyPair pair = generatorKeyPair();
|
||||
Thread client = new Client(pair);
|
||||
Thread server = new Server(pair.getPublic());
|
||||
server.start();
|
||||
client.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA算法的公私密钥对.
|
||||
*
|
||||
* @return 生成RSA算法的公私密钥对.
|
||||
*/
|
||||
public static final KeyPair generatorKeyPair() {
|
||||
KeyPairGenerator keyGen = null;
|
||||
try {
|
||||
keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
SecureRandom random = null;
|
||||
try {
|
||||
random = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
random.setSeed(53);
|
||||
keyGen.initialize(1024, random);
|
||||
KeyPair pair = keyGen.generateKeyPair();
|
||||
return pair;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟密钥交换的服务器端, 服务器端与客户端通过共享内存来交换Digital Envelope.
|
||||
*
|
||||
* @author Rich, 2012-6-14.
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
static class Server extends Thread {
|
||||
|
||||
/**
|
||||
* 实际中有可能是客户端在请求服务器端时上送了自己的公钥, 也有可能是在注册
|
||||
* 时就在服务器端登记了公钥.
|
||||
*
|
||||
* @param clientPublicKey 客户端的公钥.
|
||||
*/
|
||||
public Server(PublicKey clientPublicKey) {
|
||||
this.clientPublicKey = clientPublicKey;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Thread#run()
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
String msg = "Legend of AK47";
|
||||
Key sessionKey = KeyGeneratorDemo.generatePlainDES();
|
||||
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sessionKey);
|
||||
byte[] p = msg.getBytes("UTF-8");
|
||||
byte[] msgCipher = cipher.doFinal(p);
|
||||
cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, clientPublicKey);
|
||||
byte[] keyCipher = cipher.doFinal(sessionKey.getEncoded());
|
||||
Map result = new HashMap();
|
||||
result.put("msg", msgCipher);
|
||||
result.put("key", keyCipher);
|
||||
CHANNEL.offer(result);
|
||||
} catch (KeyException e) {
|
||||
// TODO Exception handling...
|
||||
}
|
||||
}
|
||||
|
||||
/** 客户端的公钥. */
|
||||
private final PublicKey clientPublicKey;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟密钥交换的客户端, 服务器端与客户端通过共享内存来交换Digital Envelope.
|
||||
*
|
||||
* @author Rich, 2012-6-14.
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
static class Client extends Thread {
|
||||
|
||||
/**
|
||||
* 密钥对应该在客户端内部产生, 然后客户端在请求服务器端时上送了自己的公钥, 也有可能是在注册时就在服务器端登记了公钥.
|
||||
*
|
||||
* @param keyPair 客户端的公私密钥对.
|
||||
*/
|
||||
public Client(KeyPair keyPair) {
|
||||
this.keyPair = keyPair;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see java.lang.Thread#run()
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Map received = CHANNEL.take();
|
||||
byte[] msgCipher = received.get("msg");
|
||||
byte[] keyCipher = received.get("key");
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
byte[] encoded = cipher.doFinal(keyCipher);
|
||||
KeySpec keySpec = new DESKeySpec(encoded);
|
||||
SecretKeyFactory fac = SecretKeyFactory.getInstance("DES");
|
||||
Key key = fac.generateSecret(keySpec);
|
||||
cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
byte[] msg = cipher.doFinal(msgCipher);
|
||||
String plainText = new String(msg, "UTF-8");
|
||||
System.out.println(plainText);
|
||||
} catch (Exception e) {
|
||||
// TODO Exception handling...
|
||||
}
|
||||
}
|
||||
/** 客户端的公私密钥对. */
|
||||
private final KeyPair keyPair;
|
||||
}
|
||||
|
||||
/** Client端与Server端交换数据的队列, 模拟两者之间通讯的通道, 现实中两者可能是通过Socket通讯的. */
|
||||
private static final BlockingQueue> CHANNEL = new LinkedBlockingQueue>(1);
|
||||
}
|
||||
```
|
||||
|
||||
复制
|
||||
|
||||
**KeyAgreement的DH与数字信封的RSA比较:**
|
||||
|
||||
- a. DH仅限于交换共享密钥,而无法对交换双方的身份进行认证,易受中间人攻击
|
||||
- b. RSA可以用于交换共享密钥也可以用于[身份认证](https://cloud.tencent.com/solution/tb-digitalid?from_column=20065&from=20065)
|
||||
- c. 建议:在双方都有数字证书时,使用RSA,一方或两方都没有数字证书则使用Diffie-Hellman,SSL3.0就是采用的此策略
|
||||
|
||||
## 六、总结
|
||||
|
||||
JCE中最常用和最核心的功能就是加解密,此功能由Cipher组件提供,在使用Cipher前需对加密算法及参数先做出选择:
|
||||
|
||||
### 1. 算法选择
|
||||
|
||||
对称算法一般速度较快,非对称算法速度较慢;对称算法的密钥管理比较困难,非对称算法密钥管理简单;非对称算法一般用于认证和加密会话密钥,通讯双方大部分也就是在开启会话时使用一次,对称算法一般用来加密双方之间的报文/交换的数据,使用频度较高。
|
||||
|
||||
### 2. 块/流模式选择
|
||||
|
||||
块(Block)模式加密以块为基本单位,适用于明文长度已知的情形;流(Stream)模式以bit或byte为加解密单位, 适用于明文长度未知、内容较大的情形,如加密一个套接字管道或文件读写流等,一般仅适用于硬件实现。块模式下不同算法的块大小可能不一样,一般都是2的次方数,大部分长度为64bits,整个明文长度不是块长度整倍数时,需在最后一个Block进行补长(Padding)
|
||||
|
||||
### 3. 反馈模式选择
|
||||
|
||||
使用块算法加密,如果明文有大量重复的内容,则对块加密后得到的密文也会存在大量的重复,这对密文分析、破解提供了极大的便利,为消除这方面的威胁,有个思路就是对不同块密文再进行运算,这样就极大去除了块密文与块明文几间的特征关联,这种做法称为块反馈模式。常见的反馈模式有:ECB、CBC、CFB、OFB等。对于第1个block,因没有其它块密文可供运算,有的模式引入了初始矢量(Initialization Vector,IV,由用户指定)作为第1个block内容,这样就进一步解决了第1个block密文的脆弱性。注意:尽量不要使用ECB模式。
|
||||
|
||||
### 4. 补长方案选择
|
||||
|
||||
下面是一些常见的补长(Padding)方案,以DES算法加密明文for为例,因for不足8bytes,所以需补长5bytes(for ?? ?? ?? ?? ??),则这5bytes可能选择(16进制):
|
||||
|
||||
- i. 所有Padding以长度为值:66 6F 72 05 05 05 05 05
|
||||
- ii. Padding以0×80开始后面全部为0×00:66 6F 72 80 00 00 00 00
|
||||
- iii. 最后一个字节为Padding长度, 其它为0×00:66 6f 72 00 00 00 00 05
|
||||
- iv. 全部Padding为0×00:66 6f 72 00 00 00 00 00
|
||||
- v. 全部Padding为0×20(空格):66 6f 72 20 20 20 20 20
|
||||
|
||||
JCE中支持的补长方案包括:NoPadding、PKCS5Padding、ISO10126Padding、OAEPWithAndPadding和SSL3Padding,NoPadding即不补长,其中最常用的就是PKCS5Padding和ISO10126Padding。
|
||||
|
||||
简单的说PKCS5Padding就2个规则:
|
||||
|
||||
- i. 补长的内容为待补长字节数
|
||||
- ii. 补长的字节数为:8 – 明文长度 % 8,即补长长度在1至8bytes之间
|
||||
|
||||
如前述的明文for将补长为:66 6F 72 05 05 05 05 05ISO10126Padding,具体规范请参考[ISO10126](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fwww.iso.org%2Fiso%2Fiso_catalogue%2Fcatalogue_tc%2Fcatalogue_detail.htm%3Fcsnumber%3D18114&source=article&objectId=2040606)。简单的说ISO10126Padding就是补长的长度作为补长内容的最后一个byte,之前的补长内容为随机数。
|
||||
|
||||
如前述的明文for可能补长为:66 6F 72 2A 75 EF F8 05
|
||||
|
||||
### 5. 密钥的选择
|
||||
|
||||
密钥可以使用KeyGenerator/KeyPairGenerator生成,也可以由外部导入,还可以有密钥参数构造KeySpec再转换为Key。
|
||||
|
||||
### 6. 密钥长度选择
|
||||
|
||||
对于对称加密算法,128bits的密钥足够安全,条件许可请选择256bits,注意密钥长度大于128bits需单独下载并安装jurisdiction policy files;对于非对称加密算法,1024bits的密钥足够安全。
|
||||
|
||||
最后,如选用基于口令的算法或在用户输入密码时,请避免使用String来引用,使用char[],用完立刻置空char[],避免内存攻击,如heap dump分析等。
|
||||
|
||||
参考资料:
|
||||
|
||||
- [Java安全 – JCE](https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2F87426628.blog.163.com%2Fblog%2Fstatic%2F606936182013104883902%2F&source=article&objectId=2040606)
|
||||
@@ -0,0 +1,5 @@
|
||||
java -Dcom.sun.management.jmxremote \
|
||||
-Dcom.sun.management.jmxremote.port=12345 \ # 选择一个未被占用的端口,例如 12345
|
||||
-Dcom.sun.management.jmxremote.authenticate=false \ # 关闭认证
|
||||
-Dcom.sun.management.jmxremote.ssl=false \ # 关闭SSL
|
||||
-jar your_application.jar
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
| JDK | class file version | 16 |
|
||||
| ------ | ------------------ | --- |
|
||||
| 19 | 63 | 3F |
|
||||
| 18 | 62 | 3E |
|
||||
| **17** | 61 | 3D |
|
||||
| 16 | 60 | 3C |
|
||||
| **15** | 59 | 3B |
|
||||
| 14 | 58 | 3A |
|
||||
| 13 | 57 | 39 |
|
||||
| 12 | 56 | 38 |
|
||||
| **11** | 55 | 37 |
|
||||
| 10 | 54 | 36 |
|
||||
| 9 | 53 | 35 |
|
||||
| 8 | 52 | 34 |
|
||||
| 7 | 51 | 33 |
|
||||
| 6 | 50 | 32 |
|
||||
| 5 | 49 | 31 |
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
| JDK | class file version | 16 |
|
||||
| ------ | ------------------ | --- |
|
||||
| 19 | 63 | 3F |
|
||||
| 18 | 62 | 3E |
|
||||
| **17** | 61 | 3D |
|
||||
| 16 | 60 | 3C |
|
||||
| **15** | 59 | 3B |
|
||||
| 14 | 58 | 3A |
|
||||
| 13 | 57 | 39 |
|
||||
| 12 | 56 | 38 |
|
||||
| **11** | 55 | 37 |
|
||||
| 10 | 54 | 36 |
|
||||
| 9 | 53 | 35 |
|
||||
| 8 | 52 | 34 |
|
||||
| 7 | 51 | 33 |
|
||||
| 6 | 50 | 32 |
|
||||
| 5 | 49 | 31 |
|
||||
@@ -0,0 +1,16 @@
|
||||
可以放在几个位置:
|
||||
|
||||
1. 放在自定义的一个目录,通过[虚拟机](https://so.csdn.net/so/search?q=%E8%99%9A%E6%8B%9F%E6%9C%BA&spm=1001.2101.3001.7020)参数jna.library.path指定路径
|
||||
-Djna.library.path=/xxx
|
||||
|
||||
2. 操作系统的动态链接库的位置
|
||||
|
||||
3. 放在 resources 目录。在编译之后,会在target/classes路径下。
|
||||
|
||||
并且当这几个目录同时存在的情况下,优先级从第一项到第三项依次递减。
|
||||
|
||||
使用编程方式指定路径:
|
||||
NativeLibrary.addSearchPath()
|
||||
|
||||
|
||||
window平台上则是win32-x86子目录,linux平台上则是linux-x86子目录;
|
||||
@@ -0,0 +1,167 @@
|
||||
NA的全称是Java Native Access,它为我们提供了一种更加简单的方式来访问本地的共享库资源,如果你使用JNA,那么你只需要编写相应的java代码即可,不需要编写JNI或者本地代码,非常的方便。
|
||||
|
||||
本质上JNA使用的是一个小的JNI library stub,从而能够动态调用本地方法。
|
||||
|
||||
JNA就是一个jar包,目前最新的版本是5.10.0,我们可以像下面这样引用它:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>5.10.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
一个具体使用JNA的例子:
|
||||
|
||||
```typescript
|
||||
public class JNAUsage {
|
||||
|
||||
public interface CLibrary extends Library {
|
||||
CLibrary INSTANCE = (CLibrary)
|
||||
Native.load((Platform.isWindows() ? "msvcrt" : "c"),
|
||||
CLibrary.class);
|
||||
|
||||
void printf(String format, Object... args);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CLibrary.INSTANCE.printf("Hello, World\n");
|
||||
for (int i=0;i < args.length;i++) {
|
||||
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个例子中,我们想要加载系统的c lib,从而使用c lib中的printf方法。
|
||||
|
||||
具体做法就是创建一个CLibrary interface,这个interface继承自Library,然后使用Native.load方法来加载c lib,最后在这个interface中定义要使用的lib中的方法即可。
|
||||
|
||||
那么JNA到底是怎么加载native lib的呢?我们一起来看看。
|
||||
|
||||
# JNA加载native lib的流程
|
||||
|
||||
在讲解JNA加载native lib之前,我们先回顾一下JNI是怎么加载native lib的呢?
|
||||
|
||||
在JNI中,我们首先在java代码中定义要调用的native方法,然后使用javah命令,创建C的头文件,然后再使用C或者C++来对这个头文件进行实现。
|
||||
|
||||
接下来最重要的一步就是将生成的动态链接库添加到JAVA的classpath中,从而在JAVA调用native方法的时候,能够加载到对应的库文件。
|
||||
|
||||
对于上面的JNA的例子来说,直接运行可以得到下面的结果:
|
||||
|
||||
```undefined
|
||||
Hello, World
|
||||
```
|
||||
|
||||
我们可以向程序添加JVM参数:-Djna.debug_load=true,从而让程序能够输出一些调试信息,再次运行结果如下所示:
|
||||
|
||||
```bash
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
|
||||
信息: Looking in classpath from jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7 for /com/sun/jna/darwin-aarch64/libjnidispatch.jnilib
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
|
||||
信息: Found library resource at jar:file:/Users/flydean/.m2/repository/net/java/dev/jna/jna/5.10.0/jna-5.10.0.jar!/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
|
||||
信息: Extracting library to /Users/flydean/Library/Caches/JNA/temp/jna17752159487359796115.tmp
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Looking for library 'c'
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Adding paths from jna.library.path: null
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Trying libc.dylib
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Found library 'c' at libc.dylib
|
||||
Hello, World
|
||||
```
|
||||
|
||||
仔细观察上面的输出结果,我们可以大概了解JNA的工作流程。JNA的工作流程可以分为两部分,第一部分是Library Loading,第二部分是Native Library Loading。
|
||||
|
||||
两个部分分别对应的类是com.sun.jna.Native和com.sun.jna.NativeLibrary。
|
||||
|
||||
第一部分的Library Loading意思是将jnidispatch这个共享的lib文件加载到System中,加载的顺序是这样的:
|
||||
|
||||
1. jna.boot.library.path.
|
||||
2. 使用System.loadLibrary(java.lang.String)从系统的library path中查找。如果不想从系统libary path中查找,则可以设置jna.nosys=true。
|
||||
3. 如果从上述路径中没有找到,则会调用loadNativeDispatchLibrary将jna.jar中的jnidispatch解压到本地,然后进行加载。如果不想从classpath中查找,则可以设置jna.noclasspath=true。 如果不想从jna.jar文件中解压,则可以设置jna.nounpack=true。
|
||||
4. 如果你的系统对于从jar文件中解压文件有安全方面的限制,比如SELinux,那么你需要手动将jnidispatch安装在一个可以访问的地址,然后使用1或者2的方式来设置加载方式和路径。
|
||||
|
||||
当jnidispatch被加载之后,会设置系统变量 jna.loaded=true,表示jna的lib已经加载完毕。
|
||||
|
||||
> 默认情况下我们加载的lib文件名字叫jnidispatch,你也可以通过设置jna.boot.library.name来对他进行修改。
|
||||
|
||||
我们看一下loadNativeDispatchLibrary的核心代码:
|
||||
|
||||
```java
|
||||
String libName = "/com/sun/jna/" + Platform.RESOURCE_PREFIX + "/" + mappedName;
|
||||
File lib = extractFromResourcePath(libName, Native.class.getClassLoader());
|
||||
if (lib == null) {
|
||||
if (lib == null) {
|
||||
throw new UnsatisfiedLinkError("Could not find JNA native support");
|
||||
}
|
||||
}
|
||||
|
||||
LOG.log(DEBUG_JNA_LOAD_LEVEL, "Trying {0}", lib.getAbsolutePath());
|
||||
System.setProperty("jnidispatch.path", lib.getAbsolutePath());
|
||||
System.load(lib.getAbsolutePath());
|
||||
jnidispatchPath = lib.getAbsolutePath();
|
||||
```
|
||||
|
||||
首先是查找stub lib文件:/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib, 默认情况下这个lib文件是在jna.jar包中的,所以需要调用extractFromResourcePath方法将jar包中的lib文件拷贝到临时文件中,然后调用System.load方法将其加载。
|
||||
|
||||
第二部分就是调用com.sun.jna.NativeLibrary中的loadLibrary方法来加载JAVA代码中要加载的lib。
|
||||
|
||||
在loadLibrary的时候有一些搜索路径的规则如下:
|
||||
|
||||
1. jna.library.path,用户自定义的jna lib的路径,优先从用户自定义的路径中开始查找。
|
||||
2. jna.platform.library.path, 和platform相关的lib路径。
|
||||
3. 如果是在OSX操作系统上,则会去搜索 ~/Library/Frameworks, /Library/Frameworks, 和 /System/Library/Frameworks ,去查询对应的Frameworks。
|
||||
4. 最后会去查找Context class loader classpath(classpath或者resource path),具体的格式是${os-prefix}/LIBRARY_FILENAME。如果内容是在jar包中,则会将文件解压缩至 jna.tmpdir,然后进行加载。
|
||||
|
||||
所有的搜索逻辑都放在NativeLibrary的方法loadLibrary中实现的,方法体太长了,这里就不一一列举了,感兴趣的朋友可以自行去探索。
|
||||
|
||||
# 本地方法中的结构体参数
|
||||
|
||||
如果本地方法传入的参数是基本类型的话,在JNA中定义该native方法就用基本类型即可。
|
||||
|
||||
但是有时候,本地方法本身的参数是一个结构体类型,这种情况下我们该如何进行处理呢?
|
||||
|
||||
以Windows中的kernel32 library为例,这个lib中有一个GetSystemTime方法,传入的是一个time结构体。
|
||||
|
||||
我们通过继承Structure来定义参数的结构体:
|
||||
|
||||
```cpp
|
||||
@FieldOrder({ "wYear", "wMonth", "wDayOfWeek", "wDay", "wHour", "wMinute", "wSecond", "wMilliseconds" })
|
||||
public static class SYSTEMTIME extends Structure {
|
||||
public short wYear;
|
||||
public short wMonth;
|
||||
public short wDayOfWeek;
|
||||
public short wDay;
|
||||
public short wHour;
|
||||
public short wMinute;
|
||||
public short wSecond;
|
||||
public short wMilliseconds;
|
||||
}
|
||||
```
|
||||
|
||||
然后定义一个Kernel32的interface:
|
||||
|
||||
```java
|
||||
public interface Kernel32 extends StdCallLibrary {
|
||||
Kernel32 INSTANCE = (Kernel32)
|
||||
Native.load("kernel32", Kernel32.class);
|
||||
Kernel32 SYNC_INSTANCE = (Kernel32)
|
||||
Native.synchronizedLibrary(INSTANCE);
|
||||
|
||||
void GetSystemTime(SYSTEMTIME result);
|
||||
}
|
||||
```
|
||||
|
||||
最后这样调用:
|
||||
|
||||
```vhdl
|
||||
Kernel32 lib = Kernel32.INSTANCE;
|
||||
SYSTEMTIME time = new SYSTEMTIME();
|
||||
lib.GetSystemTime(time);
|
||||
|
||||
System.out.println("Today's integer value is " + time.wDay);
|
||||
```
|
||||
@@ -0,0 +1,167 @@
|
||||
NA的全称是Java Native Access,它为我们提供了一种更加简单的方式来访问本地的共享库资源,如果你使用JNA,那么你只需要编写相应的java代码即可,不需要编写JNI或者本地代码,非常的方便。
|
||||
|
||||
本质上JNA使用的是一个小的JNI library stub,从而能够动态调用本地方法。
|
||||
|
||||
JNA就是一个jar包,目前最新的版本是5.10.0,我们可以像下面这样引用它:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>5.10.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
一个具体使用JNA的例子:
|
||||
|
||||
```typescript
|
||||
public class JNAUsage {
|
||||
|
||||
public interface CLibrary extends Library {
|
||||
CLibrary INSTANCE = (CLibrary)
|
||||
Native.load((Platform.isWindows() ? "msvcrt" : "c"),
|
||||
CLibrary.class);
|
||||
|
||||
void printf(String format, Object... args);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CLibrary.INSTANCE.printf("Hello, World\n");
|
||||
for (int i=0;i < args.length;i++) {
|
||||
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个例子中,我们想要加载系统的c lib,从而使用c lib中的printf方法。
|
||||
|
||||
具体做法就是创建一个CLibrary interface,这个interface继承自Library,然后使用Native.load方法来加载c lib,最后在这个interface中定义要使用的lib中的方法即可。
|
||||
|
||||
那么JNA到底是怎么加载native lib的呢?我们一起来看看。
|
||||
|
||||
# JNA加载native lib的流程
|
||||
|
||||
在讲解JNA加载native lib之前,我们先回顾一下JNI是怎么加载native lib的呢?
|
||||
|
||||
在JNI中,我们首先在java代码中定义要调用的native方法,然后使用javah命令,创建C的头文件,然后再使用C或者C++来对这个头文件进行实现。
|
||||
|
||||
接下来最重要的一步就是将生成的动态链接库添加到JAVA的classpath中,从而在JAVA调用native方法的时候,能够加载到对应的库文件。
|
||||
|
||||
对于上面的JNA的例子来说,直接运行可以得到下面的结果:
|
||||
|
||||
```undefined
|
||||
Hello, World
|
||||
```
|
||||
|
||||
我们可以向程序添加JVM参数:-Djna.debug_load=true,从而让程序能够输出一些调试信息,再次运行结果如下所示:
|
||||
|
||||
```bash
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
|
||||
信息: Looking in classpath from jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7 for /com/sun/jna/darwin-aarch64/libjnidispatch.jnilib
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
|
||||
信息: Found library resource at jar:file:/Users/flydean/.m2/repository/net/java/dev/jna/jna/5.10.0/jna-5.10.0.jar!/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
|
||||
信息: Extracting library to /Users/flydean/Library/Caches/JNA/temp/jna17752159487359796115.tmp
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Looking for library 'c'
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Adding paths from jna.library.path: null
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Trying libc.dylib
|
||||
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
|
||||
信息: Found library 'c' at libc.dylib
|
||||
Hello, World
|
||||
```
|
||||
|
||||
仔细观察上面的输出结果,我们可以大概了解JNA的工作流程。JNA的工作流程可以分为两部分,第一部分是Library Loading,第二部分是Native Library Loading。
|
||||
|
||||
两个部分分别对应的类是com.sun.jna.Native和com.sun.jna.NativeLibrary。
|
||||
|
||||
第一部分的Library Loading意思是将jnidispatch这个共享的lib文件加载到System中,加载的顺序是这样的:
|
||||
|
||||
1. jna.boot.library.path.
|
||||
2. 使用System.loadLibrary(java.lang.String)从系统的library path中查找。如果不想从系统libary path中查找,则可以设置jna.nosys=true。
|
||||
3. 如果从上述路径中没有找到,则会调用loadNativeDispatchLibrary将jna.jar中的jnidispatch解压到本地,然后进行加载。如果不想从classpath中查找,则可以设置jna.noclasspath=true。 如果不想从jna.jar文件中解压,则可以设置jna.nounpack=true。
|
||||
4. 如果你的系统对于从jar文件中解压文件有安全方面的限制,比如SELinux,那么你需要手动将jnidispatch安装在一个可以访问的地址,然后使用1或者2的方式来设置加载方式和路径。
|
||||
|
||||
当jnidispatch被加载之后,会设置系统变量 jna.loaded=true,表示jna的lib已经加载完毕。
|
||||
|
||||
> 默认情况下我们加载的lib文件名字叫jnidispatch,你也可以通过设置jna.boot.library.name来对他进行修改。
|
||||
|
||||
我们看一下loadNativeDispatchLibrary的核心代码:
|
||||
|
||||
```java
|
||||
String libName = "/com/sun/jna/" + Platform.RESOURCE_PREFIX + "/" + mappedName;
|
||||
File lib = extractFromResourcePath(libName, Native.class.getClassLoader());
|
||||
if (lib == null) {
|
||||
if (lib == null) {
|
||||
throw new UnsatisfiedLinkError("Could not find JNA native support");
|
||||
}
|
||||
}
|
||||
|
||||
LOG.log(DEBUG_JNA_LOAD_LEVEL, "Trying {0}", lib.getAbsolutePath());
|
||||
System.setProperty("jnidispatch.path", lib.getAbsolutePath());
|
||||
System.load(lib.getAbsolutePath());
|
||||
jnidispatchPath = lib.getAbsolutePath();
|
||||
```
|
||||
|
||||
首先是查找stub lib文件:/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib, 默认情况下这个lib文件是在jna.jar包中的,所以需要调用extractFromResourcePath方法将jar包中的lib文件拷贝到临时文件中,然后调用System.load方法将其加载。
|
||||
|
||||
第二部分就是调用com.sun.jna.NativeLibrary中的loadLibrary方法来加载JAVA代码中要加载的lib。
|
||||
|
||||
在loadLibrary的时候有一些搜索路径的规则如下:
|
||||
|
||||
1. jna.library.path,用户自定义的jna lib的路径,优先从用户自定义的路径中开始查找。
|
||||
2. jna.platform.library.path, 和platform相关的lib路径。
|
||||
3. 如果是在OSX操作系统上,则会去搜索 ~/Library/Frameworks, /Library/Frameworks, 和 /System/Library/Frameworks ,去查询对应的Frameworks。
|
||||
4. 最后会去查找Context class loader classpath(classpath或者resource path),具体的格式是${os-prefix}/LIBRARY_FILENAME。如果内容是在jar包中,则会将文件解压缩至 jna.tmpdir,然后进行加载。
|
||||
|
||||
所有的搜索逻辑都放在NativeLibrary的方法loadLibrary中实现的,方法体太长了,这里就不一一列举了,感兴趣的朋友可以自行去探索。
|
||||
|
||||
# 本地方法中的结构体参数
|
||||
|
||||
如果本地方法传入的参数是基本类型的话,在JNA中定义该native方法就用基本类型即可。
|
||||
|
||||
但是有时候,本地方法本身的参数是一个结构体类型,这种情况下我们该如何进行处理呢?
|
||||
|
||||
以Windows中的kernel32 library为例,这个lib中有一个GetSystemTime方法,传入的是一个time结构体。
|
||||
|
||||
我们通过继承Structure来定义参数的结构体:
|
||||
|
||||
```cpp
|
||||
@FieldOrder({ "wYear", "wMonth", "wDayOfWeek", "wDay", "wHour", "wMinute", "wSecond", "wMilliseconds" })
|
||||
public static class SYSTEMTIME extends Structure {
|
||||
public short wYear;
|
||||
public short wMonth;
|
||||
public short wDayOfWeek;
|
||||
public short wDay;
|
||||
public short wHour;
|
||||
public short wMinute;
|
||||
public short wSecond;
|
||||
public short wMilliseconds;
|
||||
}
|
||||
```
|
||||
|
||||
然后定义一个Kernel32的interface:
|
||||
|
||||
```java
|
||||
public interface Kernel32 extends StdCallLibrary {
|
||||
Kernel32 INSTANCE = (Kernel32)
|
||||
Native.load("kernel32", Kernel32.class);
|
||||
Kernel32 SYNC_INSTANCE = (Kernel32)
|
||||
Native.synchronizedLibrary(INSTANCE);
|
||||
|
||||
void GetSystemTime(SYSTEMTIME result);
|
||||
}
|
||||
```
|
||||
|
||||
最后这样调用:
|
||||
|
||||
```vhdl
|
||||
Kernel32 lib = Kernel32.INSTANCE;
|
||||
SYSTEMTIME time = new SYSTEMTIME();
|
||||
lib.GetSystemTime(time);
|
||||
|
||||
System.out.println("Today's integer value is " + time.wDay);
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
JUC 是 `java.util.concurrent` 包的简称,是 Java 官方提供的用于简化并发编程的工具包。它从 Java 5 开始引入,随后在 Java 7、8 等版本中持续增强,旨在帮助开发者更容易地编写正确、高性能的多线程程序。
|
||||
|
||||
### 主要包含的组件:
|
||||
|
||||
1. **原子类(Atomic Variables)**
|
||||
如 `AtomicInteger`、`AtomicLong`、`AtomicReference` 等。利用 CAS(Compare-And-Swap)机制提供线程安全的原子操作,避免了使用重量级锁。
|
||||
|
||||
2. **锁(Locks)**
|
||||
如 `ReentrantLock`、`ReentrantReadWriteLock`、`StampedLock`。相比 `synchronized`,它们提供更灵活的锁获取与释放方式(如可中断、尝试加锁、超时等),并且支持公平锁、条件变量等。
|
||||
|
||||
3. **线程池(Executors & Thread Pools)**
|
||||
包括 `ThreadPoolExecutor`、`ScheduledThreadPoolExecutor` 以及 `Executors` 工厂类。通过复用线程来降低频繁创建/销毁线程的开销,并提供任务队列、拒绝策略等管理能力。
|
||||
|
||||
4. **并发集合(Concurrent Collections)**
|
||||
如 `ConcurrentHashMap`、`CopyOnWriteArrayList`、`BlockingQueue`(`ArrayBlockingQueue`、`LinkedBlockingQueue` 等)。这些集合内部实现了高效的线程同步,避免了外部手动加锁。
|
||||
|
||||
5. **同步工具(Synchronizers)**
|
||||
如 `CountDownLatch`(等待一组线程完成)、`CyclicBarrier`(循环屏障,让多个线程互相等待)、`Semaphore`(信号量,控制并发数量)、`Exchanger`(两个线程交换数据)等。用于协调多个线程之间的协作。
|
||||
|
||||
6. **Fork/Join 框架**
|
||||
用于递归分解大任务,利用工作窃取(work-stealing)算法提高并行计算效率。`ForkJoinPool` 是其核心实现。
|
||||
|
||||
### 为什么需要 JUC?
|
||||
- 传统的 `synchronized`、`wait/notify` 等低层机制使用复杂,容易出错(死锁、性能差等)。
|
||||
- JUC 提供了经过充分测试、高性能的并发组件,开发者只需关注业务逻辑,无需重复造轮子。
|
||||
- 极大地提升了高并发场景下的开发效率和程序稳定性。
|
||||
|
||||
简单说:**JUC 是 Java 并发编程的“武器库”,让你安全、高效地编写多线程代码。**
|
||||
@@ -0,0 +1,710 @@
|
||||
|
||||
|
||||

|
||||
|
||||
> 大家好,我是 V 哥。今天看了阿里云开发者社区关于 Java 的灵魂拷问,一线大厂在用 Java 时,都会考虑哪些问题呢,对于工作多年,又没有大厂经历的小伙伴不妨看看,V 哥总结的这13个为什么,你都会哪些?先赞后看,绝不摆烂。
|
||||
|
||||
## 1. 为什么禁止使用 BigDecimal 的 equals 方法做等值比较?
|
||||
|
||||
`BigDecimal` 的 `equals` 方法在等值比较时存在一些问题,通常不建议直接使用它来判断数值的相等性。下面是主要原因以及推荐的替代方案:
|
||||
|
||||
### 1. `equals` 方法比较严格,包含了精度和符号的比较
|
||||
|
||||
`BigDecimal.equals` 不仅比较数值本身,还会比较精度和符号。例如,`BigDecimal` 的 `equals` 方法会认为 `1.0` 和 `1.00` 是不同的值,因为它们的 `scale` 不同(即小数位数不同)。例如:
|
||||
|
||||
BigDecimal a = new BigDecimal("1.0");
|
||||
BigDecimal b = new BigDecimal("1.00");
|
||||
|
||||
System.out.println(a.equals(b)); // 输出 false
|
||||
|
||||
尽管 `1.0` 和 `1.00` 数值上是相等的,但 `equals` 方法会因为精度不同返回 `false`。
|
||||
|
||||
### 2. `equals` 方法会区分正负零
|
||||
|
||||
在 `BigDecimal` 中,正零 (`0.0`) 和负零 (`-0.0`) 是不相等的,而使用 `equals` 会导致 `0.0` 和 `-0.0` 被视为不相等。例如:
|
||||
|
||||
BigDecimal zero1 = new BigDecimal("0.0");
|
||||
BigDecimal zero2 = new BigDecimal("-0.0");
|
||||
|
||||
System.out.println(zero1.equals(zero2)); // 输出 false
|
||||
|
||||
这可能会导致误判,因为在大多数业务逻辑中,我们认为 `0.0` 和 `-0.0` 是等值的。
|
||||
|
||||
### 推荐的替代方案:使用 `compareTo` 方法
|
||||
|
||||
为了避免这些问题,建议使用 `BigDecimal.compareTo` 方法。`compareTo` 方法仅比较数值的大小,不关注精度和符号。因此,在需要判断两个 `BigDecimal` 是否等值时,使用 `compareTo` 更为合理:
|
||||
|
||||
BigDecimal a = new BigDecimal("1.0");
|
||||
BigDecimal b = new BigDecimal("1.00");
|
||||
|
||||
System.out.println(a.compareTo(b) == 0); // 输出 true
|
||||
|
||||
在这种情况下,`1.0` 和 `1.00` 被视为相等,即使它们的精度不同,`compareTo` 也会返回 `0`。
|
||||
|
||||
### 小结
|
||||
|
||||
- **不要使用 `equals` 方法**:它会考虑精度和符号,容易导致误判。
|
||||
- **推荐使用 `compareTo` 方法**:只比较数值,忽略精度和正负零的差异,可以实现更符合业务需求的等值比较。
|
||||
|
||||
## 2. 为什么禁止使用double直接构造BigDecimal?
|
||||
|
||||
在使用 `BigDecimal` 时,不建议直接使用 `double` 作为构造参数。这是因为 `double` 类型在 Java 中的表示是基于二进制浮点数的,会引入精度误差,从而导致不准确的结果。例如:
|
||||
|
||||
double d = 0.1;
|
||||
BigDecimal bd = new BigDecimal(d);
|
||||
System.out.println(bd); // 输出 0.1000000000000000055511151231257827021181583404541015625
|
||||
|
||||
### 原因解析
|
||||
|
||||
1. **二进制浮点数的精度问题**
|
||||
`double` 使用 IEEE 754 标准表示小数,在二进制系统中,像 `0.1` 这样的小数无法精确表示,导致它在存储时会变成一个近似值。这个近似值会直接传递给 `BigDecimal` 的构造方法,从而生成带有误差的 `BigDecimal` 值。
|
||||
2. **结果不准确,影响业务计算**
|
||||
在一些金融计算或其他对精度要求高的场景中,直接使用 `double` 构造 `BigDecimal` 会带来潜在的误差积累,从而影响最终的结果。例如,在多次计算或累加时,误差可能不断放大。
|
||||
|
||||
### 推荐的替代方案
|
||||
|
||||
- **使用字符串或精确值构造 `BigDecimal`**
|
||||
通过传入字符串形式的数字,可以避免精度误差,因为字符串构造器不会引入任何二进制的近似计算。
|
||||
|
||||
BigDecimal bd = new BigDecimal("0.1");
|
||||
System.out.println(bd); // 输出 0.1
|
||||
|
||||
- **使用 `BigDecimal.valueOf(double)` 方法**
|
||||
另一个安全的方式是使用 `BigDecimal.valueOf(double)`,该方法会将 `double` 转换为 `String` 表示,然后构造 `BigDecimal`,从而避免精度损失。
|
||||
|
||||
BigDecimal bd = BigDecimal.valueOf(0.1);
|
||||
System.out.println(bd); // 输出 0.1
|
||||
|
||||
### 小结
|
||||
|
||||
- **避免直接使用 `double` 构造 `BigDecimal`**,以免引入二进制浮点数的精度误差。
|
||||
- **优先使用字符串构造器**,或使用 `BigDecimal.valueOf(double)` 以确保精度。
|
||||
|
||||
## 3. 为什么禁止使用 Apache Beanutils 进行属性的 copy ?
|
||||
|
||||
`Apache BeanUtils` 是一个早期用于 Java Bean 属性复制的工具库,但在现代 Java 开发中通常不推荐使用它来进行属性的拷贝,尤其在性能敏感的场景中。原因主要包括以下几点:
|
||||
|
||||
### 1. **性能问题**
|
||||
|
||||
`Apache BeanUtils.copyProperties()` 使用了大量的反射操作,且每次拷贝都需要对字段、方法进行查找和反射调用。反射机制虽然灵活,但性能较低,尤其是在大量对象或频繁拷贝的场景中,会产生显著的性能瓶颈。
|
||||
|
||||
相比之下,`Spring BeanUtils` 或 `Apache Commons Lang` 的 `FieldUtils` 等工具经过优化,使用了更高效的方式进行属性复制。在性能要求较高的场合,`MapStruct` 或 `Dozer` 等编译期代码生成的方式则可以完全避免运行时反射。
|
||||
|
||||
### 2. **类型转换问题**
|
||||
|
||||
`BeanUtils.copyProperties` 在属性类型不匹配时会隐式地进行类型转换。例如,将 `String` 类型的 `"123"` 转换为 `Integer`,如果转换失败,会抛出异常。这种隐式转换在处理数据时,可能带来不易察觉的错误,而且并不总是适合应用场景。
|
||||
|
||||
在精确的属性复制需求下,通常希望类型不匹配时直接跳过拷贝,或明确抛出错误,而不是隐式转换。例如,`Spring BeanUtils.copyProperties` 不会进行隐式转换,适合严格的属性匹配场景。
|
||||
|
||||
### 3. **潜在的安全问题**
|
||||
|
||||
`Apache BeanUtils` 的 `PropertyUtils` 组件在执行反射操作时存在一定的安全隐患。历史上,`BeanUtils` 的 `PropertyUtils` 曾有安全漏洞,使恶意用户可以通过精心构造的输入利用反射机制执行系统命令或加载恶意类。尽管这些漏洞在现代版本中已得到修复,但该库的架构和实现仍较为陈旧,难以应对更高的安全需求。
|
||||
|
||||
### 4. **缺乏对嵌套对象的深拷贝支持**
|
||||
|
||||
`BeanUtils.copyProperties` 仅支持浅拷贝,即只能复制对象的一级属性,无法递归地对嵌套对象进行复制。如果对象包含了复杂的嵌套结构,使用 `BeanUtils.copyProperties` 很容易出现意外行为或数据丢失。像 `MapStruct` 或 `Dozer` 这样的工具则提供对嵌套对象的深层复制能力,更适合复杂对象的深度拷贝需求。
|
||||
|
||||
### 推荐的替代方案
|
||||
|
||||
1. **Spring `BeanUtils.copyProperties()`**
|
||||
Spring 的 `BeanUtils.copyProperties()` 提供了更优的性能和更好的类型安全性。它不做类型转换,且提供了方便的过滤器用于选择性拷贝属性。
|
||||
2. **MapStruct**
|
||||
`MapStruct` 是基于注解的对象映射框架,支持编译期生成代码,完全避免了反射的性能开销,且支持复杂对象、嵌套属性的深度拷贝,是性能要求较高的首选。
|
||||
3. **Dozer**
|
||||
`Dozer` 支持更灵活的映射配置和深拷贝,适合对象结构复杂的情况。它可以处理嵌套属性映射、类型转换,且具有较好的自定义能力。
|
||||
|
||||
### 小结
|
||||
|
||||
`Apache BeanUtils.copyProperties` 不适合现代 Java 开发的性能、安全性和灵活性要求,推荐使用更高效、安全、灵活的框架(如 Spring `BeanUtils`、`MapStruct` 等)来代替。
|
||||
|
||||
## 4. 为什么要求日期格式化时必须有使用 y 表示年,而不能用 Y ?
|
||||
|
||||
在日期格式化中,必须使用 `y` 而不是 `Y` 来表示年份,这是因为 `y` 和 `Y` 在 Java 和其他日期格式化工具中代表不同的含义:
|
||||
|
||||
1. **`y` 表示日历年(Calendar Year)**
|
||||
`y` 是标准的表示年份的字符,表示的是通常意义上的公历年,比如 `2024` 表示的就是这一年的年份。使用 `y` 时,日期格式化工具会准确地格式化出对应的年份数值:
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
System.out.println(sdf.format(new Date())); // 输出: 2024-11-10
|
||||
|
||||
2. **`Y` 表示星期年(Week Year)**
|
||||
`Y` 表示的是“星期年”或称“ISO周年”(ISO week-numbering year),它是一种基于ISO周数的年份表示方式。这种表示法根据每年的第一个星期一所在的周来计算年份,如果某天属于新一年的第一个完整星期,则会归为新年的星期年。
|
||||
|
||||
例如,如果某年的最后几天在下一年开始的第一个星期中,它们可能会被归入下一年的 `week year`。同理,如果新年的前几天在上一年的最后一个完整星期内,这些天的星期年可能会归属上一年。这在日期和时间处理中可能导致意外的年份差异。
|
||||
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
|
||||
System.out.println(sdf.format(new Date())); // 可能输出与实际年份不同的值
|
||||
|
||||
### 使用 `Y` 的潜在问题
|
||||
|
||||
使用 `Y` 表示年份会引发一些日期计算的错误,因为它依赖于周数的计算方式,不是每次都与实际的公历年份一致。例如:
|
||||
|
||||
- 2024年12月31日会被视作 `2025` 的 `week year`,导致使用 `YYYY` 格式化时得到 `2025-12-31`。
|
||||
- 在跨年计算或特定日期逻辑中使用 `Y` 表示年份可能会出现错误,因为 `week year` 与通常理解的日历年并不总是相符。
|
||||
|
||||
### 什么时候使用 `Y`
|
||||
|
||||
`Y` 一般仅用于需要符合 ISO 8601 标准的日期格式,特别是包含 ISO 周数(如“2024-W01-1”表示2024年的第一个星期一)的情况,而在一般情况下,我们都应使用 `y` 来表示日历年份。
|
||||
|
||||
### 小结
|
||||
|
||||
- **使用 `y` 来表示常规年份**,避免日期格式化错误。
|
||||
- **避免使用 `Y` 来表示年份**,除非确实需要按照 ISO 周年的格式来解析和显示年份。
|
||||
|
||||
## 5. 为什么使用三目运算符时必需要注意类型对齐?
|
||||
|
||||
在使用三目运算符时,类型对齐非常重要,因为三目运算符的两个分支会被类型推断成一个共同的类型。若两者类型不同,Java 编译器会进行类型提升或自动转换,这可能导致意外的类型变化和潜在的错误。以下是需要注意的原因和细节:
|
||||
|
||||
### 1. 三目运算符会自动进行类型提升
|
||||
|
||||
三目运算符的返回值类型是根据 `true` 和 `false` 分支的类型推断出来的。为了得到一致的结果,Java 会自动将不同的类型提升为更高精度的类型。例如,若一个分支返回 `int` 而另一个分支返回 `double`,Java 会将 `int` 提升为 `double`:
|
||||
|
||||
int x = 5;
|
||||
double y = 10.5;
|
||||
double result = (x > 0) ? x : y; // 返回 double 类型
|
||||
System.out.println(result); // 输出 5.0
|
||||
|
||||
这里返回值 `5` 被提升为 `5.0`。虽然代码在这个例子中不会出错,但在某些情况下,这种自动提升会导致意外的精度损失或类型不匹配的问题。
|
||||
|
||||
### 2. 自动拆箱和装箱可能引发 `NullPointerException`
|
||||
|
||||
在 Java 中,基本类型和包装类型的对齐需要特别小心。三目运算符会尝试将包装类型和基本类型对齐成相同类型,这会导致自动装箱和拆箱,如果某个分支为 `null` 且需要拆箱,可能会引发 `NullPointerException`:
|
||||
|
||||
Integer a = null;
|
||||
int b = 10;
|
||||
int result = (a != null) ? a : b; // 如果 a 为 null,结果会发生自动拆箱,引发 NullPointerException
|
||||
|
||||
由于 `a` 为 `null`,Java 会尝试将其拆箱为 `int`,从而抛出 `NullPointerException`。为避免这种情况,可以确保类型对齐,或避免对可能为 `null` 的对象进行拆箱。
|
||||
|
||||
### 3. 返回值类型不一致可能导致编译错误
|
||||
|
||||
如果三目运算符的两种返回类型无法被编译器自动转换为一个兼容类型,代码会直接报错。例如:
|
||||
|
||||
int x = 5;
|
||||
String y = "10";
|
||||
Object result = (x > 0) ? x : y; // 编译错误:int 和 String 不兼容
|
||||
|
||||
在这种情况下,`int` 和 `String` 无法被提升到相同类型,因此会引发编译错误。若确实希望返回不同类型的值,可以手动指定共同的超类型,例如将结果定义为 `Object` 类型:
|
||||
|
||||
Object result = (x > 0) ? Integer.valueOf(x) : y; // 这里 result 为 Object
|
||||
|
||||
### 4. 类型对齐可以提升代码的可读性
|
||||
|
||||
保持三目运算符返回的类型一致,能让代码更加清晰,便于理解和维护。类型对齐可以避免类型转换和自动提升带来的混乱,使代码更容易预测和理解:
|
||||
|
||||
double result = (condition) ? 1.0 : 0.0; // 返回 double
|
||||
|
||||
### 小结
|
||||
|
||||
- **保持类型一致性**,确保 `true` 和 `false` 分支的类型相同,避免意外的类型提升。
|
||||
- **小心自动装箱和拆箱**,避免 `null` 参与三目运算符计算。
|
||||
- **在返回不同类型时选择合适的公共类型**,如使用 `Object` 或显式转换。
|
||||
|
||||
## 6. 为什么建议初始化 HashMap 的容量大小?
|
||||
|
||||
初始化 `HashMap` 的容量大小是为了提高性能和减少内存浪费。通过设置合适的初始容量,可以减少 `HashMap` 的扩容次数,提高程序运行效率。以下是详细原因和建议:
|
||||
|
||||
### 1. 减少扩容次数,提高性能
|
||||
|
||||
`HashMap` 默认的初始容量为 16,当超过负载因子阈值(默认是 0.75,即达到容量的 75%)时,`HashMap` 会自动进行扩容操作,将容量扩大为原来的两倍。扩容涉及到重新计算哈希并将数据重新分布到新的桶中,这个过程非常耗时,尤其在元素较多时,扩容会显著影响性能。
|
||||
|
||||
通过设置合适的初始容量,可以避免或减少扩容操作,提高 `HashMap` 的存取效率。
|
||||
|
||||
### 2. 节省内存,避免不必要的内存开销
|
||||
|
||||
如果预计要存储大量数据但没有指定容量,`HashMap` 可能会多次扩容,每次扩容会分配新的内存空间,并将原有数据复制到新空间中,造成内存浪费。如果在创建 `HashMap` 时能合理估算其容量,则可以一次性分配足够的空间,从而避免重复分配内存带来的资源浪费。
|
||||
|
||||
### 3. 避免扩容带来的线程安全问题
|
||||
|
||||
在并发环境下,频繁扩容可能导致线程不安全,即使是 `ConcurrentHashMap` 也不能完全避免扩容带来的性能和一致性问题。初始化合适的容量可以减少并发环境下扩容带来的风险。
|
||||
|
||||
### 如何估算合适的容量
|
||||
|
||||
1. **预估数据量**:如果预计 `HashMap` 将存储 `n` 个元素,可以将初始容量设置为 `(n / 0.75)`,再向上取整为最接近的 2 的幂次方。
|
||||
|
||||
int initialCapacity = (int) Math.ceil(n / 0.75);
|
||||
Map<String, String> map = new HashMap<>(initialCapacity);
|
||||
|
||||
2. **取 2 的幂次方**:`HashMap` 的容量总是以 2 的幂次方增长,因为在进行哈希运算时,可以高效利用按位与操作来计算哈希桶索引。因此,初始容量设为 2 的幂次方会使哈希分布更均匀。
|
||||
|
||||
### 示例代码
|
||||
|
||||
int expectedSize = 1000; // 预估需要存储的键值对数量
|
||||
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
|
||||
HashMap<String, Integer> map = new HashMap<>(initialCapacity);
|
||||
|
||||
### 小结
|
||||
|
||||
初始化 `HashMap` 的容量大小有以下好处:
|
||||
|
||||
- **提高性能**:减少扩容次数,优化存取效率。
|
||||
- **节省内存**:避免多次扩容引起的内存浪费。
|
||||
- **提升线程安全**:在并发环境下减少扩容带来的线程不安全风险。
|
||||
|
||||
合理初始化 `HashMap` 容量对于高性能应用尤为重要,尤其在存储大量数据时可以显著提升程序的运行效率。
|
||||
|
||||
## 7. 为什么禁止使用 Executors 创建线程池?
|
||||
|
||||
在 Java 中创建线程池时,不推荐直接使用 `Executors` 提供的快捷方法(例如 `Executors.newFixedThreadPool()`、`Executors.newCachedThreadPool()` 等),而推荐使用 `ThreadPoolExecutor` 构造方法来手动配置线程池。这种做法主要是为了避免 `Executors` 创建线程池时隐藏的风险,确保线程池配置符合需求。具体原因如下:
|
||||
|
||||
### 1. **不透明的任务队列长度导致OOM风险**
|
||||
|
||||
- **`newFixedThreadPool()` 和 `newSingleThreadExecutor()`** 使用的是**无界队列** `LinkedBlockingQueue`。无界队列可以存放无限数量的任务,一旦任务量非常大,队列会迅速占用大量内存,导致 **OutOfMemoryError**(OOM)。
|
||||
- **`newCachedThreadPool()`** 使用的是 **SynchronousQueue**,该队列没有存储任务的能力,每个任务到来时必须立即有一个空闲线程来处理任务,否则将创建一个新线程。当任务到达速度超过线程销毁速度时,线程数量会快速增加,导致 **OOM**。
|
||||
|
||||
### 2. **线程数无法控制,导致资源耗尽**
|
||||
|
||||
在 `newCachedThreadPool()` 创建的线程池中,线程数没有上限,短时间内大量请求会导致线程数暴增,耗尽系统资源。`newFixedThreadPool()` 和 `newSingleThreadExecutor()` 虽然限制了核心线程数,但未限制任务队列长度,依然可能耗尽内存。
|
||||
|
||||
在业务需求不确定或任务激增的场景下,建议明确限制线程池的最大线程数和队列长度,以更好地控制系统资源的使用,避免因线程数无法控制导致的性能问题。
|
||||
|
||||
### 3. **缺乏合理的拒绝策略控制**
|
||||
|
||||
- `Executors` 创建的线程池默认使用 `AbortPolicy` 拒绝策略,即当线程池达到饱和时会抛出 `RejectedExecutionException` 异常。
|
||||
- 不同的业务场景可能需要不同的拒绝策略,例如可以使用 `CallerRunsPolicy`(让提交任务的线程执行任务)或 `DiscardOldestPolicy`(丢弃最旧的任务)来平衡任务处理。
|
||||
|
||||
手动创建 `ThreadPoolExecutor` 时,可以指定适合业务需求的拒绝策略,从而更灵活地处理线程池满载的情况,避免异常或系统性能下降。
|
||||
|
||||
### 4. **灵活配置核心参数**
|
||||
|
||||
使用 `ThreadPoolExecutor` 的构造方法可以手动设置以下参数,以便根据业务需求灵活配置线程池:
|
||||
|
||||
- **corePoolSize**:核心线程数,避免空闲线程被频繁销毁和重建。
|
||||
- **maximumPoolSize**:最大线程数,控制线程池能使用的最大资源。
|
||||
- **keepAliveTime**:非核心线程的存活时间,适合控制线程销毁频率。
|
||||
- **workQueue**:任务队列类型和长度,便于管理任务积压的情况。
|
||||
|
||||
这些参数的合理配置可以有效平衡线程池的性能、资源占用和任务处理能力,避免使用默认配置时不符合需求的情况。
|
||||
|
||||
### 推荐的线程池创建方式
|
||||
|
||||
建议直接使用 `ThreadPoolExecutor` 构造方法配置线程池,例如:
|
||||
|
||||
int corePoolSize = 10;
|
||||
int maximumPoolSize = 20;
|
||||
long keepAliveTime = 60L;
|
||||
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
|
||||
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
corePoolSize,
|
||||
maximumPoolSize,
|
||||
keepAliveTime,
|
||||
TimeUnit.SECONDS,
|
||||
workQueue,
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
### 小结
|
||||
|
||||
使用 `Executors` 创建线程池会带来不易察觉的风险,可能导致系统资源耗尽或任务堆积,手动配置 `ThreadPoolExecutor` 可以更好地控制线程池的行为,使其符合实际业务需求和资源限制。因此,为了系统的健壮性和可控性,建议避免使用 `Executors` 快捷方法来创建线程池。
|
||||
|
||||
## 8. 为什么要求谨慎使用 ArrayList 中的 subList 方法?
|
||||
|
||||
在使用 `ArrayList` 的 `subList` 方法时需要谨慎,因为它有一些潜在的陷阱,容易导致意外的错误和难以排查的异常。以下是 `subList` 需要小心使用的原因和注意事项:
|
||||
|
||||
### 1. `subList` 返回的是视图,而不是独立副本
|
||||
|
||||
`ArrayList` 的 `subList` 方法返回的是原列表的一部分视图(`view`),而不是一个独立的副本。对 `subList` 的修改会直接影响原列表,反之亦然:
|
||||
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
|
||||
List<Integer> subList = list.subList(1, 4);
|
||||
subList.set(0, 10); // 修改 subList
|
||||
System.out.println(list); // 原列表也受到影响:[1, 10, 3, 4, 5]
|
||||
|
||||
这种共享视图的机制在某些场景中可能引发意外的修改,导致数据被意外改变,从而影响到原始数据结构的完整性和正确性。
|
||||
|
||||
### 2. `subList` 的结构性修改限制
|
||||
|
||||
当对 `ArrayList` 本身(而非 `subList` 视图)进行结构性修改(`add`、`remove` 等改变列表大小的操作)后,再操作 `subList` 会导致 `ConcurrentModificationException` 异常。这是因为 `subList` 和原 `ArrayList` 之间共享结构性修改的状态,一旦其中一个发生修改,另一方就会失效:
|
||||
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
|
||||
List<Integer> subList = list.subList(1, 4);
|
||||
list.add(6); // 修改原列表的结构
|
||||
subList.get(0); // 抛出 ConcurrentModificationException
|
||||
|
||||
这种限制意味着 `subList` 不适合在列表频繁变化的场景中使用,否则很容易引发并发修改异常。
|
||||
|
||||
### 3. `subList` 和 `ArrayList` 的 removeAll 等操作可能导致错误
|
||||
|
||||
`subList` 生成的视图列表可能会在批量删除操作中出现问题,例如调用 `removeAll` 方法时,`subList` 的行为不一致或发生异常。对于 `ArrayList` 的 `subList`,一些批量修改方法(如 `removeAll`、`retainAll`)可能会在删除视图元素后,导致 `ArrayList` 产生不可预料的状态,甚至引发 `IndexOutOfBoundsException` 等异常。
|
||||
|
||||
### 4. 推荐的安全使用方式
|
||||
|
||||
如果需要一个独立的子列表,可以通过 `new ArrayList<>(originalList.subList(start, end))` 来创建一个子列表的副本,从而避免 `subList` 的共享视图问题:
|
||||
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
|
||||
ArrayList<Integer> subListCopy = new ArrayList<>(list.subList(1, 4)); // 创建副本
|
||||
list.add(6); // 修改原列表
|
||||
subListCopy.get(0); // 安全,不会受到影响
|
||||
|
||||
### 小结
|
||||
|
||||
使用 `ArrayList` 的 `subList` 方法需要注意以下几点:
|
||||
|
||||
- **视图机制**:`subList` 只是原列表的视图,修改其中一个会影响另一个。
|
||||
- **结构性修改限制**:结构性修改原列表后再访问 `subList` 会抛出 `ConcurrentModificationException`。
|
||||
- **批量操作问题**:`subList` 的批量操作可能引发不可预料的错误。
|
||||
- **建议创建副本**:如需独立操作子列表,最好创建 `subList` 的副本以避免潜在问题。
|
||||
|
||||
谨慎使用 `subList` 可以避免意外的错误,提高代码的健壮性。
|
||||
|
||||
## 9. 为什么禁止在 foreach 循环里进行元素的 remove/add 操作?
|
||||
|
||||
在 Java 中,禁止在 `foreach` 循环中进行元素的 `remove` 或 `add` 操作,主要是因为这种操作可能导致 `ConcurrentModificationException` 异常,或者导致循环行为不符合预期。具体原因如下:
|
||||
|
||||
### 1. **`ConcurrentModificationException` 异常**
|
||||
|
||||
当你在 `foreach` 循环中直接修改集合(例如 `remove` 或 `add` 元素),会导致并发修改问题。`foreach` 循环底层使用了集合的 `Iterator` 来遍历元素。大多数集合类(如 `ArrayList`、`HashSet` 等)都会维护一个 `modCount` 计数器,表示集合的结构变更次数。当你在遍历时修改集合的结构(如删除或添加元素),`modCount` 会发生变化,而 `Iterator` 会检测到这种结构性修改,从而抛出 `ConcurrentModificationException` 异常,防止程序在多线程环境中出现意外行为。
|
||||
|
||||
例如:
|
||||
|
||||
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
|
||||
for (String s : list) {
|
||||
if (s.equals("b")) {
|
||||
list.remove(s); // 会抛出 ConcurrentModificationException
|
||||
}
|
||||
}
|
||||
|
||||
在上面的代码中,`foreach` 循环遍历 `list` 时,如果删除了元素 `b`,它会修改 `list` 的结构,从而导致 `Iterator` 检测到并发修改,抛出异常。
|
||||
|
||||
### 2. **不可预测的行为**
|
||||
|
||||
即使没有抛出 `ConcurrentModificationException`,在 `foreach` 循环中修改集合也会导致不可预测的行为。例如,`remove` 或 `add` 操作会改变集合的大小和内容,可能会影响迭代的顺序或导致遗漏某些元素,甚至造成死循环或跳过某些元素。
|
||||
|
||||
例如:
|
||||
|
||||
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
|
||||
for (String s : list) {
|
||||
if (s.equals("b")) {
|
||||
list.add("e"); // 修改集合的大小
|
||||
}
|
||||
System.out.println(s);
|
||||
}
|
||||
|
||||
在这个例子中,`add` 操作会向 `list` 中添加一个新元素 `"e"`,从而修改了集合的结构。因为 `foreach` 循环的内部实现使用了迭代器,它可能不会考虑到修改后的新元素,导致输出顺序或遍历结果与预期不同。
|
||||
|
||||
### 3. **迭代器的 `remove()` 方法**
|
||||
|
||||
如果需要在循环中删除元素,推荐使用 `Iterator` 显式地进行删除操作。`Iterator` 提供了一个安全的 `remove()` 方法,可以在遍历时安全地删除元素,而不会引发 `ConcurrentModificationException`。
|
||||
|
||||
例如:
|
||||
|
||||
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
|
||||
Iterator<String> iterator = list.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String s = iterator.next();
|
||||
if (s.equals("b")) {
|
||||
iterator.remove(); // 使用 Iterator 的 remove() 方法
|
||||
}
|
||||
}
|
||||
|
||||
使用 `Iterator.remove()` 可以安全地在遍历时删除元素,而不会抛出并发修改异常。
|
||||
|
||||
### 小结
|
||||
|
||||
在 `foreach` 循环中直接进行 `remove` 或 `add` 操作是不安全的,主要有以下原因:
|
||||
|
||||
- **`ConcurrentModificationException`**:直接修改集合会触发迭代器的并发修改检测,导致异常。
|
||||
- **不可预测的行为**:修改集合的结构可能导致元素遗漏、顺序错乱或程序逻辑出错。
|
||||
- **使用 `Iterator` 替代**:使用 `Iterator` 的 `remove()` 方法可以避免这些问题,实现安全的元素删除操作。
|
||||
|
||||
因此,正确的做法是使用 `Iterator` 显式地处理元素的删除或修改,而不是直接在 `foreach` 循环中进行修改。
|
||||
|
||||
## 10. 为什么禁止工程师直接使用日志系统 (Log4j、Logback) 中的 API ?
|
||||
|
||||
在很多工程实践中,**禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API**,主要是出于以下几个原因:
|
||||
|
||||
### 1. **日志配置与实现的分离**
|
||||
|
||||
直接使用日志系统的 API 可能会导致日志记录逻辑与应用的业务逻辑紧密耦合,使得日志配置和实现的分离变得困难。现代的日志框架(如 Log4j、Logback)允许通过外部配置文件(如 `log4j.xml` 或 `logback.xml`)灵活配置日志级别、输出格式、输出位置等,而不是硬编码到应用代码中。直接使用日志 API 会导致日志的配置与业务代码绑定在一起,不易修改和维护。
|
||||
|
||||
**建议的做法**:通过使用日志框架的日志抽象接口(如 `org.slf4j.Logger`)来记录日志,而不是直接依赖具体的日志实现。这种方式提供了更大的灵活性,日志实现可以在运行时通过配置文件更换而无需修改代码。
|
||||
|
||||
### 2. **灵活性与可扩展性问题**
|
||||
|
||||
如果工程师直接使用日志库的 API,项目在需要切换日志框架(比如从 Log4j 转换到 Logback 或其他框架)时,需要修改大量的代码,增加了系统的耦合度和维护难度。另一方面,使用日志抽象层(如 SLF4J)可以避免这一问题,因为 SLF4J 是一个日志抽象层,底层可以切换具体的日志实现而无需改变业务代码。
|
||||
|
||||
**示例**:
|
||||
|
||||
// 不推荐:直接使用 Log4j 的 API
|
||||
import org.apache.log4j.Logger;
|
||||
Logger logger = Logger.getLogger(MyClass.class);
|
||||
logger.info("This is a log message");
|
||||
|
||||
// 推荐:通过 SLF4J 接口来记录日志
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
Logger logger = LoggerFactory.getLogger(MyClass.class);
|
||||
logger.info("This is a log message");
|
||||
|
||||
使用 SLF4J 可以在不同的环境中灵活切换日志实现,而无需修改代码。
|
||||
|
||||
### 3. **日志记录与调试不一致**
|
||||
|
||||
如果工程师直接使用日志框架的 API,可能会在日志记录时不遵循一致的日志策略。例如,日志的级别、格式、日志输出的内容等可能不统一,导致日志信息混乱、不易追踪。通过统一的日志抽象接口(如 SLF4J)和规范的日志记录策略(通过 AOP 或日志框架自带的特性)可以保持日志的一致性和规范性。
|
||||
|
||||
**最佳实践**:
|
||||
|
||||
- 通过统一的日志管理类或工具类来封装日志记录方法,确保所有日志记录都遵循统一的格式和规范。
|
||||
- 在日志中统一使用适当的日志级别(如 `DEBUG`、`INFO`、`WARN`、`ERROR`)和标准格式。
|
||||
|
||||
### 4. **日志的性能影响**
|
||||
|
||||
日志记录可能对应用的性能产生一定的影响,尤其是在日志记录过于频繁或日志输出内容过多的情况下。通过直接使用日志框架的 API,可能无法灵活控制日志输出的频率、内容或过滤策略,从而造成性能问题。很多日志框架(如 Log4j 和 Logback)提供了高级的配置选项,如异步日志、日志缓存等特性,可以显著提高性能。
|
||||
|
||||
**推荐做法**:
|
||||
|
||||
- 使用日志框架提供的异步日志功能来提高性能。
|
||||
- 配置适当的日志级别,避免在生产环境中输出过多的调试信息。
|
||||
|
||||
### 5. **日志管理的统一性与规范**
|
||||
|
||||
在团队开发中,直接使用日志框架的 API 会导致不同开发人员在不同模块中记录日志时不遵循统一规范,导致日志格式不统一、信息不一致,甚至产生重复的日志记录。通过日志管理工具类或封装类,可以确保所有开发人员遵循统一的日志记录策略。
|
||||
|
||||
**示例**:
|
||||
|
||||
- 创建一个统一的 `LoggerFactory` 工厂类来生成日志记录对象。
|
||||
- 统一定义日志级别和输出格式,确保日志输出一致。
|
||||
|
||||
### 小结
|
||||
|
||||
禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API,主要是为了:
|
||||
|
||||
1. **解耦日志实现与业务逻辑**:通过使用日志抽象层(如 SLF4J),可以更轻松地切换日志框架,避免硬编码。
|
||||
2. **提高灵活性与可维护性**:避免在应用中重复使用框架 API,提高日志配置的灵活性和一致性。
|
||||
3. **规范日志记录行为**:通过封装日志记录,确保日志级别、格式和内容的统一,增强可读性和可追踪性。
|
||||
4. **优化性能**:通过配置日志框架的高级功能(如异步日志),提高日志系统的性能,减少对应用的影响。
|
||||
5. **统一日志管理**:避免团队成员在不同模块中使用不一致的日志记录方式,确保日志输出的标准化。
|
||||
|
||||
最好的做法是通过日志抽象层(如 SLF4J)进行日志记录,同时通过日志管理工具类进行统一的配置和调用,确保日志的高效、规范和灵活性。
|
||||
|
||||
## 11. 为什么建议开发者谨慎使用继承?
|
||||
|
||||
在面向对象编程(OOP)中,**继承**是一种常见的代码复用方式,它允许一个类继承另一个类的属性和行为。然而,虽然继承可以提高代码的复用性,但过度或不当使用继承可能会导致代码的复杂性增加,进而带来一些潜在的问题。因此,建议开发者在使用继承时要谨慎,以下是一些关键原因:
|
||||
|
||||
### 1. **增加了类之间的耦合性**
|
||||
|
||||
继承会导致子类和父类之间形成紧密的耦合关系。子类依赖于父类的实现,这意味着如果父类发生变化,可能会影响到所有继承自该父类的子类,导致修改和维护变得更加困难。这种紧密耦合关系也限制了子类的灵活性,因为它必须遵循父类的接口和实现。
|
||||
|
||||
**例子**:
|
||||
|
||||
class Animal {
|
||||
void eat() {
|
||||
System.out.println("Animal is eating");
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
@Override
|
||||
void eat() {
|
||||
System.out.println("Dog is eating");
|
||||
}
|
||||
}
|
||||
|
||||
如果父类 `Animal` 做了改动(如修改 `eat()` 方法的实现),`Dog` 类也会受到影响。这样的耦合会增加后期维护的复杂度。
|
||||
|
||||
### 2. **破坏了封装性(Encapsulation)**
|
||||
|
||||
继承可能破坏封装性,因为子类可以直接访问父类的成员(字段和方法),尤其是当父类成员被设置为 `protected` 或 `public` 时。这种情况可能导致子类暴露不应被外界访问的细节,破坏了数据的封装性。
|
||||
|
||||
**例子**:
|
||||
|
||||
class Vehicle {
|
||||
protected int speed;
|
||||
}
|
||||
|
||||
class Car extends Vehicle {
|
||||
void accelerate() {
|
||||
speed += 10; // 直接访问父类的 protected 字段
|
||||
}
|
||||
}
|
||||
|
||||
在这种情况下,`Car` 类直接访问了父类 `Vehicle` 的 `speed` 字段,而不是通过公共接口来修改它,导致封装性降低。
|
||||
|
||||
### 3. **继承可能会导致类的层次结构不合理**
|
||||
|
||||
继承往往会导致不合理的类层次结构,特别是在试图通过继承来表达“是一个”(`is-a`)关系时,实际情况可能并不符合这种逻辑。滥用继承可能会使类之间的关系变得复杂和不直观,导致代码结构混乱。
|
||||
|
||||
**例子**:
|
||||
假设我们有一个 `Car` 类和一个 `Truck` 类,都继承自 `Vehicle` 类。如果 `Car` 和 `Truck` 共享很多方法和属性,这样的设计可能是合适的。但是,如果 `Car` 和 `Truck` 之间差异很大,仅通过继承来构建它们的关系,可能会导致继承层次过于复杂,代码阅读和理解变得困难。
|
||||
|
||||
### 4. **继承可能导致不易发现的错误**
|
||||
|
||||
由于子类继承了父类的行为,任何对父类的修改都有可能影响到子类的行为。更糟糕的是,错误或不一致的修改可能在父类中发生,而这些错误可能不会立即暴露出来,直到程序运行到某个特定的地方,才会显现出错误。
|
||||
|
||||
**例子**:
|
||||
假设你修改了父类的某个方法,但忘记更新或调整子类中相应的重写方法,这可能会导致难以发现的错误。
|
||||
|
||||
### 5. **继承限制了灵活性(不可重用性问题)**
|
||||
|
||||
继承创建了一个父类与子类之间的固定关系,这意味着如果你想在一个完全不同的上下文中重用一个类,你可能不能通过继承来实现。在某些情况下,组合比继承更为灵活,允许你将多个行为组合到一个类中,而不是通过继承来强行构建类的层次结构。
|
||||
|
||||
**例子**:
|
||||
|
||||
// 组合而非继承
|
||||
class Engine {
|
||||
void start() {
|
||||
System.out.println("Engine started");
|
||||
}
|
||||
}
|
||||
|
||||
class Car {
|
||||
private Engine engine = new Engine(); // 通过组合来使用 Engine
|
||||
void start() {
|
||||
engine.start();
|
||||
}
|
||||
}
|
||||
|
||||
通过组合,可以灵活地使用不同的组件,而不需要继承整个类。这样做的优点是更具扩展性和灵活性。
|
||||
|
||||
### 6. **继承限制了方法的重用(可维护性差)**
|
||||
|
||||
如果你过度依赖继承,你的代码会容易受到父类实现的限制,难以灵活地添加新功能或进行扩展。例如,在继承链中添加新的功能可能会导致一大堆方法的修改和重写,而不通过继承,可以更轻松地将功能作为独立模块来重用。
|
||||
|
||||
### 7. **使用接口和组合更优**
|
||||
|
||||
相比继承,**接口(Interface)** 和 **组合(Composition)** 更符合面向对象设计的原则。接口允许类只暴露所需的功能,而不暴露实现细节,组合则允许你将多个不同的行为组合在一起,使得系统更加灵活和可扩展。通过接口和组合,可以避免继承的许多问题。
|
||||
|
||||
**推荐设计模式**:
|
||||
|
||||
- **策略模式(Strategy Pattern)**:通过接口和组合来替代继承。
|
||||
- **装饰器模式(Decorator Pattern)**:使用组合和代理来扩展行为,而非通过继承。
|
||||
|
||||
### 小结
|
||||
|
||||
尽管继承是面向对象编程中的一个重要特性,但滥用继承可能带来许多问题,特别是在以下几个方面:
|
||||
|
||||
- 增加类之间的耦合,降低灵活性;
|
||||
- 破坏封装性,暴露不应访问的内部实现;
|
||||
- 可能导致类层次结构复杂,增加理解和维护的难度;
|
||||
- 限制代码的重用和扩展性。
|
||||
|
||||
因此,**推荐优先使用组合而非继承**,并尽可能使用接口来实现灵活的扩展。如果必须使用继承,确保它能够清晰地表达“是一个”的关系,并避免过深的继承层次。
|
||||
|
||||
## 12. 为什么禁止开发人员修改 serialVersionUID 字段的值?
|
||||
|
||||
`serialVersionUID` 是 Java 中用来标识序列化版本的一个静态字段。它的作用是确保在反序列化时,JVM 可以验证序列化的类与当前类的兼容性,以避免版本不兼容导致的错误。尽管 `serialVersionUID` 可以由开发人员手动定义,**禁止开发人员修改 `serialVersionUID` 字段的值** 的原因如下:
|
||||
|
||||
### 1. **序列化与反序列化兼容性**
|
||||
|
||||
`serialVersionUID` 的主要作用是保证在序列化和反序列化过程中,类的版本兼容性。它是用来标识类的版本的,如果序列化和反序列化过程中使用的类的 `serialVersionUID` 不匹配,就会抛出 `InvalidClassException`。
|
||||
|
||||
- **不匹配的 `serialVersionUID`** 会导致序列化的数据与当前类不兼容,导致反序列化失败。
|
||||
- **修改 `serialVersionUID`** 的值会改变类的版本标识,导致已序列化的数据在反序列化时不能成功读取,特别是在类结构发生改变(例如添加或删除字段)时。
|
||||
|
||||
**例如:**
|
||||
|
||||
// 类的第一次版本
|
||||
public class MyClass implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String name;
|
||||
// 其他字段和方法
|
||||
}
|
||||
|
||||
// 类的第二次修改版本
|
||||
public class MyClass implements Serializable {
|
||||
private static final long serialVersionUID = 2L; // 修改了 serialVersionUID
|
||||
private String name;
|
||||
private int age; // 新增字段
|
||||
// 其他字段和方法
|
||||
}
|
||||
|
||||
如果修改了 `serialVersionUID`,而之前序列化的数据是使用版本 1 的类进行序列化的,反序列化时会因为 `serialVersionUID` 不匹配而导致失败。
|
||||
|
||||
### 2. **避免不必要的版本冲突**
|
||||
|
||||
Java 会根据类的字段、方法等信息自动生成 `serialVersionUID`,这个值是基于类的结构计算出来的。如果开发人员修改了 `serialVersionUID`,可能会破坏 Java 自动生成的版本控制机制,从而导致版本控制不一致,增加了维护复杂性。
|
||||
|
||||
如果手动修改 `serialVersionUID`,容易出现以下几种问题:
|
||||
|
||||
- 由于类结构没有变化,修改 `serialVersionUID` 可能会导致已序列化的数据无法恢复。
|
||||
- 如果不同的开发人员修改了 `serialVersionUID`,可能会在不同的机器或系统间引起序列化不一致。
|
||||
|
||||
### 3. **影响序列化兼容性**
|
||||
|
||||
Java 提供了两种主要的兼容性规则:
|
||||
|
||||
- **兼容性向前**:如果类的字段或方法发生改变,但没有改变 `serialVersionUID`,则反序列化是可以工作的。
|
||||
- **兼容性向后**:如果你修改了类的结构(如字段变动、方法签名改变等),并且保持相同的 `serialVersionUID`,反序列化仍然可以工作。
|
||||
|
||||
如果不小心修改了 `serialVersionUID`,可能导致以下情况:
|
||||
|
||||
- **向前兼容性**:新版本的类不能兼容老版本的对象,导致反序列化失败。
|
||||
- **向后兼容性**:老版本的类无法反序列化新版本的对象。
|
||||
|
||||
### 4. **自动生成 vs 手动指定**
|
||||
|
||||
- **自动生成的 `serialVersionUID`**:Java 会根据类的结构自动生成 `serialVersionUID`,这样如果类的结构发生变化,`serialVersionUID` 会自动变化,确保不兼容的版本之间不会出现意外的反序列化行为。
|
||||
- **手动指定 `serialVersionUID`**:手动修改 `serialVersionUID` 可能导致版本控制不一致,特别是在多人开发、分布式部署的环境中,容易出现反序列化失败的问题。
|
||||
|
||||
### 5. **避免非预期的反序列化问题**
|
||||
|
||||
手动修改 `serialVersionUID` 可能会导致数据丢失或反序列化时抛出异常。例如,如果开发人员错误地修改了 `serialVersionUID`,系统在尝试反序列化时可能会因为 `serialVersionUID` 不匹配而无法成功加载对象,导致异常的发生。
|
||||
|
||||
### 小结
|
||||
|
||||
**禁止开发人员修改 `serialVersionUID` 字段的值,主要是为了:**
|
||||
|
||||
- **确保序列化与反序列化的兼容性**,避免版本不匹配导致反序列化失败。
|
||||
- **避免不必要的版本冲突和数据丢失**,特别是在类结构修改时。
|
||||
- **保持 Java 自动管理 `serialVersionUID` 的优势**,保证类的版本一致性和可维护性。
|
||||
|
||||
如果确实需要修改 `serialVersionUID`,应确保修改后的版本与已经序列化的数据兼容,并遵循合理的版本管理策略。
|
||||
|
||||
## 13. 为什么禁止开发人员使用 isSuccess 作为变量名?
|
||||
|
||||
禁止开发人员使用 `isSuccess` 作为变量名,主要是为了遵循更好的编程规范和提高代码的可读性、可维护性。这个变量名问题的核心在于其容易引起歧义和混淆。具体原因如下:
|
||||
|
||||
### 1. **不符合布尔值命名约定**
|
||||
|
||||
在 Java 中,通常使用 `is` 或 `has` 开头的变量名来表示布尔值(`boolean` 类型)。这类命名通常遵循特定的语义约定,表示某个条件是否成立。例如:
|
||||
|
||||
- `isEnabled` 表示某个功能是否启用;
|
||||
- `hasPermission` 表示是否有权限。
|
||||
|
||||
**问题**:
|
||||
|
||||
- `isSuccess` 看起来像一个布尔值(`boolean` 类型),但它实际上可能并不直接表示一个布尔值,而是一个状态或结果。这种命名可能会导致混淆,开发者可能误以为它是布尔类型的变量,而实际上它可能是一个描述状态的对象、字符串或者其他类型的数据。
|
||||
|
||||
### 2. **语义不明确**
|
||||
|
||||
`isSuccess` 这个名字表面上表示“是否成功”,但是它缺少具体的上下文,导致语义不够明确。真正表示是否成功的布尔值应该直接使用 `boolean` 类型的变量,并且使用清晰明确的命名。
|
||||
|
||||
例如:
|
||||
|
||||
- `isCompleted`:表示某个任务是否完成。
|
||||
- `isSuccessful`:表示某个操作是否成功。
|
||||
|
||||
这些命名能更明确地表达布尔变量的含义,避免理解上的歧义。
|
||||
|
||||
### 3. **与标准的 `is` 前缀混淆**
|
||||
|
||||
`is` 前缀通常用来表示“是否”某个条件成立,适用于返回布尔值的方法或者变量。`isSuccess` 这样的命名会让开发人员误以为它是一个布尔值,或者一个 `boolean` 类型的值,但实际上它可能是一个复杂类型或者其他非布尔类型,造成不必要的混淆。
|
||||
|
||||
例如:
|
||||
|
||||
boolean isSuccess = someMethod(); // 看起来是布尔值,但实际类型可能不同
|
||||
|
||||
这种情况可能导致开发人员产生误解,认为 `isSuccess` 代表的是布尔值,但它可能是某个表示成功的对象、枚举或者其他数据类型。
|
||||
|
||||
### 4. **更好的命名建议**
|
||||
|
||||
为了避免歧义和混淆,开发人员应使用更加明确且符合命名规范的名称。以下是一些命名的改进建议:
|
||||
|
||||
- 如果是布尔值,命名为 `isSuccessful` 或 `wasSuccessful`。
|
||||
- 如果是表示结果的对象,使用更具体的名称,例如 `operationResult` 或 `statusCode`,以表明它是一个描述操作结果的变量。
|
||||
|
||||
### 5. **提升代码的可读性和可维护性**
|
||||
|
||||
清晰且具有意义的命名能够帮助团队成员或未来的开发者更快地理解代码的意图。如果变量名过于模糊(如 `isSuccess`),就可能让人对其实际含义产生疑问,尤其是在阅读较大或复杂的代码时。良好的命名能够提升代码的可读性和可维护性。
|
||||
|
||||
### 小结
|
||||
|
||||
- **`isSuccess`** 这样的命名不清晰,容易与布尔类型的变量产生混淆,进而影响代码的可读性。
|
||||
- **命名应尽量明确**,避免使用容易引起歧义的名称,特别是在布尔值类型的命名时。
|
||||
- 建议使用更具描述性的名称,如 **`isSuccessful`** 或 **`wasSuccessful`**,更清晰地表达变量的意义。
|
||||
|
||||
## 最后
|
||||
|
||||
以上是 V 哥精心总结的13个 Java 编程中的小小编码问题,也是V 哥日常编码中总结的学习笔记,分享给大家,如果内容对你有帮助,请不要吝啬来个小赞呗,关注威哥爱编程,Java 路上,你我相伴前行。
|
||||
@@ -0,0 +1,710 @@
|
||||
|
||||
|
||||

|
||||
|
||||
> 大家好,我是 V 哥。今天看了阿里云开发者社区关于 Java 的灵魂拷问,一线大厂在用 Java 时,都会考虑哪些问题呢,对于工作多年,又没有大厂经历的小伙伴不妨看看,V 哥总结的这13个为什么,你都会哪些?先赞后看,绝不摆烂。
|
||||
|
||||
## 1. 为什么禁止使用 BigDecimal 的 equals 方法做等值比较?
|
||||
|
||||
`BigDecimal` 的 `equals` 方法在等值比较时存在一些问题,通常不建议直接使用它来判断数值的相等性。下面是主要原因以及推荐的替代方案:
|
||||
|
||||
### 1. `equals` 方法比较严格,包含了精度和符号的比较
|
||||
|
||||
`BigDecimal.equals` 不仅比较数值本身,还会比较精度和符号。例如,`BigDecimal` 的 `equals` 方法会认为 `1.0` 和 `1.00` 是不同的值,因为它们的 `scale` 不同(即小数位数不同)。例如:
|
||||
|
||||
BigDecimal a = new BigDecimal("1.0");
|
||||
BigDecimal b = new BigDecimal("1.00");
|
||||
|
||||
System.out.println(a.equals(b)); // 输出 false
|
||||
|
||||
尽管 `1.0` 和 `1.00` 数值上是相等的,但 `equals` 方法会因为精度不同返回 `false`。
|
||||
|
||||
### 2. `equals` 方法会区分正负零
|
||||
|
||||
在 `BigDecimal` 中,正零 (`0.0`) 和负零 (`-0.0`) 是不相等的,而使用 `equals` 会导致 `0.0` 和 `-0.0` 被视为不相等。例如:
|
||||
|
||||
BigDecimal zero1 = new BigDecimal("0.0");
|
||||
BigDecimal zero2 = new BigDecimal("-0.0");
|
||||
|
||||
System.out.println(zero1.equals(zero2)); // 输出 false
|
||||
|
||||
这可能会导致误判,因为在大多数业务逻辑中,我们认为 `0.0` 和 `-0.0` 是等值的。
|
||||
|
||||
### 推荐的替代方案:使用 `compareTo` 方法
|
||||
|
||||
为了避免这些问题,建议使用 `BigDecimal.compareTo` 方法。`compareTo` 方法仅比较数值的大小,不关注精度和符号。因此,在需要判断两个 `BigDecimal` 是否等值时,使用 `compareTo` 更为合理:
|
||||
|
||||
BigDecimal a = new BigDecimal("1.0");
|
||||
BigDecimal b = new BigDecimal("1.00");
|
||||
|
||||
System.out.println(a.compareTo(b) == 0); // 输出 true
|
||||
|
||||
在这种情况下,`1.0` 和 `1.00` 被视为相等,即使它们的精度不同,`compareTo` 也会返回 `0`。
|
||||
|
||||
### 小结
|
||||
|
||||
- **不要使用 `equals` 方法**:它会考虑精度和符号,容易导致误判。
|
||||
- **推荐使用 `compareTo` 方法**:只比较数值,忽略精度和正负零的差异,可以实现更符合业务需求的等值比较。
|
||||
|
||||
## 2. 为什么禁止使用double直接构造BigDecimal?
|
||||
|
||||
在使用 `BigDecimal` 时,不建议直接使用 `double` 作为构造参数。这是因为 `double` 类型在 Java 中的表示是基于二进制浮点数的,会引入精度误差,从而导致不准确的结果。例如:
|
||||
|
||||
double d = 0.1;
|
||||
BigDecimal bd = new BigDecimal(d);
|
||||
System.out.println(bd); // 输出 0.1000000000000000055511151231257827021181583404541015625
|
||||
|
||||
### 原因解析
|
||||
|
||||
1. **二进制浮点数的精度问题**
|
||||
`double` 使用 IEEE 754 标准表示小数,在二进制系统中,像 `0.1` 这样的小数无法精确表示,导致它在存储时会变成一个近似值。这个近似值会直接传递给 `BigDecimal` 的构造方法,从而生成带有误差的 `BigDecimal` 值。
|
||||
2. **结果不准确,影响业务计算**
|
||||
在一些金融计算或其他对精度要求高的场景中,直接使用 `double` 构造 `BigDecimal` 会带来潜在的误差积累,从而影响最终的结果。例如,在多次计算或累加时,误差可能不断放大。
|
||||
|
||||
### 推荐的替代方案
|
||||
|
||||
- **使用字符串或精确值构造 `BigDecimal`**
|
||||
通过传入字符串形式的数字,可以避免精度误差,因为字符串构造器不会引入任何二进制的近似计算。
|
||||
|
||||
BigDecimal bd = new BigDecimal("0.1");
|
||||
System.out.println(bd); // 输出 0.1
|
||||
|
||||
- **使用 `BigDecimal.valueOf(double)` 方法**
|
||||
另一个安全的方式是使用 `BigDecimal.valueOf(double)`,该方法会将 `double` 转换为 `String` 表示,然后构造 `BigDecimal`,从而避免精度损失。
|
||||
|
||||
BigDecimal bd = BigDecimal.valueOf(0.1);
|
||||
System.out.println(bd); // 输出 0.1
|
||||
|
||||
### 小结
|
||||
|
||||
- **避免直接使用 `double` 构造 `BigDecimal`**,以免引入二进制浮点数的精度误差。
|
||||
- **优先使用字符串构造器**,或使用 `BigDecimal.valueOf(double)` 以确保精度。
|
||||
|
||||
## 3. 为什么禁止使用 Apache Beanutils 进行属性的 copy ?
|
||||
|
||||
`Apache BeanUtils` 是一个早期用于 Java Bean 属性复制的工具库,但在现代 Java 开发中通常不推荐使用它来进行属性的拷贝,尤其在性能敏感的场景中。原因主要包括以下几点:
|
||||
|
||||
### 1. **性能问题**
|
||||
|
||||
`Apache BeanUtils.copyProperties()` 使用了大量的反射操作,且每次拷贝都需要对字段、方法进行查找和反射调用。反射机制虽然灵活,但性能较低,尤其是在大量对象或频繁拷贝的场景中,会产生显著的性能瓶颈。
|
||||
|
||||
相比之下,`Spring BeanUtils` 或 `Apache Commons Lang` 的 `FieldUtils` 等工具经过优化,使用了更高效的方式进行属性复制。在性能要求较高的场合,`MapStruct` 或 `Dozer` 等编译期代码生成的方式则可以完全避免运行时反射。
|
||||
|
||||
### 2. **类型转换问题**
|
||||
|
||||
`BeanUtils.copyProperties` 在属性类型不匹配时会隐式地进行类型转换。例如,将 `String` 类型的 `"123"` 转换为 `Integer`,如果转换失败,会抛出异常。这种隐式转换在处理数据时,可能带来不易察觉的错误,而且并不总是适合应用场景。
|
||||
|
||||
在精确的属性复制需求下,通常希望类型不匹配时直接跳过拷贝,或明确抛出错误,而不是隐式转换。例如,`Spring BeanUtils.copyProperties` 不会进行隐式转换,适合严格的属性匹配场景。
|
||||
|
||||
### 3. **潜在的安全问题**
|
||||
|
||||
`Apache BeanUtils` 的 `PropertyUtils` 组件在执行反射操作时存在一定的安全隐患。历史上,`BeanUtils` 的 `PropertyUtils` 曾有安全漏洞,使恶意用户可以通过精心构造的输入利用反射机制执行系统命令或加载恶意类。尽管这些漏洞在现代版本中已得到修复,但该库的架构和实现仍较为陈旧,难以应对更高的安全需求。
|
||||
|
||||
### 4. **缺乏对嵌套对象的深拷贝支持**
|
||||
|
||||
`BeanUtils.copyProperties` 仅支持浅拷贝,即只能复制对象的一级属性,无法递归地对嵌套对象进行复制。如果对象包含了复杂的嵌套结构,使用 `BeanUtils.copyProperties` 很容易出现意外行为或数据丢失。像 `MapStruct` 或 `Dozer` 这样的工具则提供对嵌套对象的深层复制能力,更适合复杂对象的深度拷贝需求。
|
||||
|
||||
### 推荐的替代方案
|
||||
|
||||
1. **Spring `BeanUtils.copyProperties()`**
|
||||
Spring 的 `BeanUtils.copyProperties()` 提供了更优的性能和更好的类型安全性。它不做类型转换,且提供了方便的过滤器用于选择性拷贝属性。
|
||||
2. **MapStruct**
|
||||
`MapStruct` 是基于注解的对象映射框架,支持编译期生成代码,完全避免了反射的性能开销,且支持复杂对象、嵌套属性的深度拷贝,是性能要求较高的首选。
|
||||
3. **Dozer**
|
||||
`Dozer` 支持更灵活的映射配置和深拷贝,适合对象结构复杂的情况。它可以处理嵌套属性映射、类型转换,且具有较好的自定义能力。
|
||||
|
||||
### 小结
|
||||
|
||||
`Apache BeanUtils.copyProperties` 不适合现代 Java 开发的性能、安全性和灵活性要求,推荐使用更高效、安全、灵活的框架(如 Spring `BeanUtils`、`MapStruct` 等)来代替。
|
||||
|
||||
## 4. 为什么要求日期格式化时必须有使用 y 表示年,而不能用 Y ?
|
||||
|
||||
在日期格式化中,必须使用 `y` 而不是 `Y` 来表示年份,这是因为 `y` 和 `Y` 在 Java 和其他日期格式化工具中代表不同的含义:
|
||||
|
||||
1. **`y` 表示日历年(Calendar Year)**
|
||||
`y` 是标准的表示年份的字符,表示的是通常意义上的公历年,比如 `2024` 表示的就是这一年的年份。使用 `y` 时,日期格式化工具会准确地格式化出对应的年份数值:
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
System.out.println(sdf.format(new Date())); // 输出: 2024-11-10
|
||||
|
||||
2. **`Y` 表示星期年(Week Year)**
|
||||
`Y` 表示的是“星期年”或称“ISO周年”(ISO week-numbering year),它是一种基于ISO周数的年份表示方式。这种表示法根据每年的第一个星期一所在的周来计算年份,如果某天属于新一年的第一个完整星期,则会归为新年的星期年。
|
||||
|
||||
例如,如果某年的最后几天在下一年开始的第一个星期中,它们可能会被归入下一年的 `week year`。同理,如果新年的前几天在上一年的最后一个完整星期内,这些天的星期年可能会归属上一年。这在日期和时间处理中可能导致意外的年份差异。
|
||||
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
|
||||
System.out.println(sdf.format(new Date())); // 可能输出与实际年份不同的值
|
||||
|
||||
### 使用 `Y` 的潜在问题
|
||||
|
||||
使用 `Y` 表示年份会引发一些日期计算的错误,因为它依赖于周数的计算方式,不是每次都与实际的公历年份一致。例如:
|
||||
|
||||
- 2024年12月31日会被视作 `2025` 的 `week year`,导致使用 `YYYY` 格式化时得到 `2025-12-31`。
|
||||
- 在跨年计算或特定日期逻辑中使用 `Y` 表示年份可能会出现错误,因为 `week year` 与通常理解的日历年并不总是相符。
|
||||
|
||||
### 什么时候使用 `Y`
|
||||
|
||||
`Y` 一般仅用于需要符合 ISO 8601 标准的日期格式,特别是包含 ISO 周数(如“2024-W01-1”表示2024年的第一个星期一)的情况,而在一般情况下,我们都应使用 `y` 来表示日历年份。
|
||||
|
||||
### 小结
|
||||
|
||||
- **使用 `y` 来表示常规年份**,避免日期格式化错误。
|
||||
- **避免使用 `Y` 来表示年份**,除非确实需要按照 ISO 周年的格式来解析和显示年份。
|
||||
|
||||
## 5. 为什么使用三目运算符时必需要注意类型对齐?
|
||||
|
||||
在使用三目运算符时,类型对齐非常重要,因为三目运算符的两个分支会被类型推断成一个共同的类型。若两者类型不同,Java 编译器会进行类型提升或自动转换,这可能导致意外的类型变化和潜在的错误。以下是需要注意的原因和细节:
|
||||
|
||||
### 1. 三目运算符会自动进行类型提升
|
||||
|
||||
三目运算符的返回值类型是根据 `true` 和 `false` 分支的类型推断出来的。为了得到一致的结果,Java 会自动将不同的类型提升为更高精度的类型。例如,若一个分支返回 `int` 而另一个分支返回 `double`,Java 会将 `int` 提升为 `double`:
|
||||
|
||||
int x = 5;
|
||||
double y = 10.5;
|
||||
double result = (x > 0) ? x : y; // 返回 double 类型
|
||||
System.out.println(result); // 输出 5.0
|
||||
|
||||
这里返回值 `5` 被提升为 `5.0`。虽然代码在这个例子中不会出错,但在某些情况下,这种自动提升会导致意外的精度损失或类型不匹配的问题。
|
||||
|
||||
### 2. 自动拆箱和装箱可能引发 `NullPointerException`
|
||||
|
||||
在 Java 中,基本类型和包装类型的对齐需要特别小心。三目运算符会尝试将包装类型和基本类型对齐成相同类型,这会导致自动装箱和拆箱,如果某个分支为 `null` 且需要拆箱,可能会引发 `NullPointerException`:
|
||||
|
||||
Integer a = null;
|
||||
int b = 10;
|
||||
int result = (a != null) ? a : b; // 如果 a 为 null,结果会发生自动拆箱,引发 NullPointerException
|
||||
|
||||
由于 `a` 为 `null`,Java 会尝试将其拆箱为 `int`,从而抛出 `NullPointerException`。为避免这种情况,可以确保类型对齐,或避免对可能为 `null` 的对象进行拆箱。
|
||||
|
||||
### 3. 返回值类型不一致可能导致编译错误
|
||||
|
||||
如果三目运算符的两种返回类型无法被编译器自动转换为一个兼容类型,代码会直接报错。例如:
|
||||
|
||||
int x = 5;
|
||||
String y = "10";
|
||||
Object result = (x > 0) ? x : y; // 编译错误:int 和 String 不兼容
|
||||
|
||||
在这种情况下,`int` 和 `String` 无法被提升到相同类型,因此会引发编译错误。若确实希望返回不同类型的值,可以手动指定共同的超类型,例如将结果定义为 `Object` 类型:
|
||||
|
||||
Object result = (x > 0) ? Integer.valueOf(x) : y; // 这里 result 为 Object
|
||||
|
||||
### 4. 类型对齐可以提升代码的可读性
|
||||
|
||||
保持三目运算符返回的类型一致,能让代码更加清晰,便于理解和维护。类型对齐可以避免类型转换和自动提升带来的混乱,使代码更容易预测和理解:
|
||||
|
||||
double result = (condition) ? 1.0 : 0.0; // 返回 double
|
||||
|
||||
### 小结
|
||||
|
||||
- **保持类型一致性**,确保 `true` 和 `false` 分支的类型相同,避免意外的类型提升。
|
||||
- **小心自动装箱和拆箱**,避免 `null` 参与三目运算符计算。
|
||||
- **在返回不同类型时选择合适的公共类型**,如使用 `Object` 或显式转换。
|
||||
|
||||
## 6. 为什么建议初始化 HashMap 的容量大小?
|
||||
|
||||
初始化 `HashMap` 的容量大小是为了提高性能和减少内存浪费。通过设置合适的初始容量,可以减少 `HashMap` 的扩容次数,提高程序运行效率。以下是详细原因和建议:
|
||||
|
||||
### 1. 减少扩容次数,提高性能
|
||||
|
||||
`HashMap` 默认的初始容量为 16,当超过负载因子阈值(默认是 0.75,即达到容量的 75%)时,`HashMap` 会自动进行扩容操作,将容量扩大为原来的两倍。扩容涉及到重新计算哈希并将数据重新分布到新的桶中,这个过程非常耗时,尤其在元素较多时,扩容会显著影响性能。
|
||||
|
||||
通过设置合适的初始容量,可以避免或减少扩容操作,提高 `HashMap` 的存取效率。
|
||||
|
||||
### 2. 节省内存,避免不必要的内存开销
|
||||
|
||||
如果预计要存储大量数据但没有指定容量,`HashMap` 可能会多次扩容,每次扩容会分配新的内存空间,并将原有数据复制到新空间中,造成内存浪费。如果在创建 `HashMap` 时能合理估算其容量,则可以一次性分配足够的空间,从而避免重复分配内存带来的资源浪费。
|
||||
|
||||
### 3. 避免扩容带来的线程安全问题
|
||||
|
||||
在并发环境下,频繁扩容可能导致线程不安全,即使是 `ConcurrentHashMap` 也不能完全避免扩容带来的性能和一致性问题。初始化合适的容量可以减少并发环境下扩容带来的风险。
|
||||
|
||||
### 如何估算合适的容量
|
||||
|
||||
1. **预估数据量**:如果预计 `HashMap` 将存储 `n` 个元素,可以将初始容量设置为 `(n / 0.75)`,再向上取整为最接近的 2 的幂次方。
|
||||
|
||||
int initialCapacity = (int) Math.ceil(n / 0.75);
|
||||
Map<String, String> map = new HashMap<>(initialCapacity);
|
||||
|
||||
2. **取 2 的幂次方**:`HashMap` 的容量总是以 2 的幂次方增长,因为在进行哈希运算时,可以高效利用按位与操作来计算哈希桶索引。因此,初始容量设为 2 的幂次方会使哈希分布更均匀。
|
||||
|
||||
### 示例代码
|
||||
|
||||
int expectedSize = 1000; // 预估需要存储的键值对数量
|
||||
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
|
||||
HashMap<String, Integer> map = new HashMap<>(initialCapacity);
|
||||
|
||||
### 小结
|
||||
|
||||
初始化 `HashMap` 的容量大小有以下好处:
|
||||
|
||||
- **提高性能**:减少扩容次数,优化存取效率。
|
||||
- **节省内存**:避免多次扩容引起的内存浪费。
|
||||
- **提升线程安全**:在并发环境下减少扩容带来的线程不安全风险。
|
||||
|
||||
合理初始化 `HashMap` 容量对于高性能应用尤为重要,尤其在存储大量数据时可以显著提升程序的运行效率。
|
||||
|
||||
## 7. 为什么禁止使用 Executors 创建线程池?
|
||||
|
||||
在 Java 中创建线程池时,不推荐直接使用 `Executors` 提供的快捷方法(例如 `Executors.newFixedThreadPool()`、`Executors.newCachedThreadPool()` 等),而推荐使用 `ThreadPoolExecutor` 构造方法来手动配置线程池。这种做法主要是为了避免 `Executors` 创建线程池时隐藏的风险,确保线程池配置符合需求。具体原因如下:
|
||||
|
||||
### 1. **不透明的任务队列长度导致OOM风险**
|
||||
|
||||
- **`newFixedThreadPool()` 和 `newSingleThreadExecutor()`** 使用的是**无界队列** `LinkedBlockingQueue`。无界队列可以存放无限数量的任务,一旦任务量非常大,队列会迅速占用大量内存,导致 **OutOfMemoryError**(OOM)。
|
||||
- **`newCachedThreadPool()`** 使用的是 **SynchronousQueue**,该队列没有存储任务的能力,每个任务到来时必须立即有一个空闲线程来处理任务,否则将创建一个新线程。当任务到达速度超过线程销毁速度时,线程数量会快速增加,导致 **OOM**。
|
||||
|
||||
### 2. **线程数无法控制,导致资源耗尽**
|
||||
|
||||
在 `newCachedThreadPool()` 创建的线程池中,线程数没有上限,短时间内大量请求会导致线程数暴增,耗尽系统资源。`newFixedThreadPool()` 和 `newSingleThreadExecutor()` 虽然限制了核心线程数,但未限制任务队列长度,依然可能耗尽内存。
|
||||
|
||||
在业务需求不确定或任务激增的场景下,建议明确限制线程池的最大线程数和队列长度,以更好地控制系统资源的使用,避免因线程数无法控制导致的性能问题。
|
||||
|
||||
### 3. **缺乏合理的拒绝策略控制**
|
||||
|
||||
- `Executors` 创建的线程池默认使用 `AbortPolicy` 拒绝策略,即当线程池达到饱和时会抛出 `RejectedExecutionException` 异常。
|
||||
- 不同的业务场景可能需要不同的拒绝策略,例如可以使用 `CallerRunsPolicy`(让提交任务的线程执行任务)或 `DiscardOldestPolicy`(丢弃最旧的任务)来平衡任务处理。
|
||||
|
||||
手动创建 `ThreadPoolExecutor` 时,可以指定适合业务需求的拒绝策略,从而更灵活地处理线程池满载的情况,避免异常或系统性能下降。
|
||||
|
||||
### 4. **灵活配置核心参数**
|
||||
|
||||
使用 `ThreadPoolExecutor` 的构造方法可以手动设置以下参数,以便根据业务需求灵活配置线程池:
|
||||
|
||||
- **corePoolSize**:核心线程数,避免空闲线程被频繁销毁和重建。
|
||||
- **maximumPoolSize**:最大线程数,控制线程池能使用的最大资源。
|
||||
- **keepAliveTime**:非核心线程的存活时间,适合控制线程销毁频率。
|
||||
- **workQueue**:任务队列类型和长度,便于管理任务积压的情况。
|
||||
|
||||
这些参数的合理配置可以有效平衡线程池的性能、资源占用和任务处理能力,避免使用默认配置时不符合需求的情况。
|
||||
|
||||
### 推荐的线程池创建方式
|
||||
|
||||
建议直接使用 `ThreadPoolExecutor` 构造方法配置线程池,例如:
|
||||
|
||||
int corePoolSize = 10;
|
||||
int maximumPoolSize = 20;
|
||||
long keepAliveTime = 60L;
|
||||
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
|
||||
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
corePoolSize,
|
||||
maximumPoolSize,
|
||||
keepAliveTime,
|
||||
TimeUnit.SECONDS,
|
||||
workQueue,
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
### 小结
|
||||
|
||||
使用 `Executors` 创建线程池会带来不易察觉的风险,可能导致系统资源耗尽或任务堆积,手动配置 `ThreadPoolExecutor` 可以更好地控制线程池的行为,使其符合实际业务需求和资源限制。因此,为了系统的健壮性和可控性,建议避免使用 `Executors` 快捷方法来创建线程池。
|
||||
|
||||
## 8. 为什么要求谨慎使用 ArrayList 中的 subList 方法?
|
||||
|
||||
在使用 `ArrayList` 的 `subList` 方法时需要谨慎,因为它有一些潜在的陷阱,容易导致意外的错误和难以排查的异常。以下是 `subList` 需要小心使用的原因和注意事项:
|
||||
|
||||
### 1. `subList` 返回的是视图,而不是独立副本
|
||||
|
||||
`ArrayList` 的 `subList` 方法返回的是原列表的一部分视图(`view`),而不是一个独立的副本。对 `subList` 的修改会直接影响原列表,反之亦然:
|
||||
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
|
||||
List<Integer> subList = list.subList(1, 4);
|
||||
subList.set(0, 10); // 修改 subList
|
||||
System.out.println(list); // 原列表也受到影响:[1, 10, 3, 4, 5]
|
||||
|
||||
这种共享视图的机制在某些场景中可能引发意外的修改,导致数据被意外改变,从而影响到原始数据结构的完整性和正确性。
|
||||
|
||||
### 2. `subList` 的结构性修改限制
|
||||
|
||||
当对 `ArrayList` 本身(而非 `subList` 视图)进行结构性修改(`add`、`remove` 等改变列表大小的操作)后,再操作 `subList` 会导致 `ConcurrentModificationException` 异常。这是因为 `subList` 和原 `ArrayList` 之间共享结构性修改的状态,一旦其中一个发生修改,另一方就会失效:
|
||||
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
|
||||
List<Integer> subList = list.subList(1, 4);
|
||||
list.add(6); // 修改原列表的结构
|
||||
subList.get(0); // 抛出 ConcurrentModificationException
|
||||
|
||||
这种限制意味着 `subList` 不适合在列表频繁变化的场景中使用,否则很容易引发并发修改异常。
|
||||
|
||||
### 3. `subList` 和 `ArrayList` 的 removeAll 等操作可能导致错误
|
||||
|
||||
`subList` 生成的视图列表可能会在批量删除操作中出现问题,例如调用 `removeAll` 方法时,`subList` 的行为不一致或发生异常。对于 `ArrayList` 的 `subList`,一些批量修改方法(如 `removeAll`、`retainAll`)可能会在删除视图元素后,导致 `ArrayList` 产生不可预料的状态,甚至引发 `IndexOutOfBoundsException` 等异常。
|
||||
|
||||
### 4. 推荐的安全使用方式
|
||||
|
||||
如果需要一个独立的子列表,可以通过 `new ArrayList<>(originalList.subList(start, end))` 来创建一个子列表的副本,从而避免 `subList` 的共享视图问题:
|
||||
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
|
||||
ArrayList<Integer> subListCopy = new ArrayList<>(list.subList(1, 4)); // 创建副本
|
||||
list.add(6); // 修改原列表
|
||||
subListCopy.get(0); // 安全,不会受到影响
|
||||
|
||||
### 小结
|
||||
|
||||
使用 `ArrayList` 的 `subList` 方法需要注意以下几点:
|
||||
|
||||
- **视图机制**:`subList` 只是原列表的视图,修改其中一个会影响另一个。
|
||||
- **结构性修改限制**:结构性修改原列表后再访问 `subList` 会抛出 `ConcurrentModificationException`。
|
||||
- **批量操作问题**:`subList` 的批量操作可能引发不可预料的错误。
|
||||
- **建议创建副本**:如需独立操作子列表,最好创建 `subList` 的副本以避免潜在问题。
|
||||
|
||||
谨慎使用 `subList` 可以避免意外的错误,提高代码的健壮性。
|
||||
|
||||
## 9. 为什么禁止在 foreach 循环里进行元素的 remove/add 操作?
|
||||
|
||||
在 Java 中,禁止在 `foreach` 循环中进行元素的 `remove` 或 `add` 操作,主要是因为这种操作可能导致 `ConcurrentModificationException` 异常,或者导致循环行为不符合预期。具体原因如下:
|
||||
|
||||
### 1. **`ConcurrentModificationException` 异常**
|
||||
|
||||
当你在 `foreach` 循环中直接修改集合(例如 `remove` 或 `add` 元素),会导致并发修改问题。`foreach` 循环底层使用了集合的 `Iterator` 来遍历元素。大多数集合类(如 `ArrayList`、`HashSet` 等)都会维护一个 `modCount` 计数器,表示集合的结构变更次数。当你在遍历时修改集合的结构(如删除或添加元素),`modCount` 会发生变化,而 `Iterator` 会检测到这种结构性修改,从而抛出 `ConcurrentModificationException` 异常,防止程序在多线程环境中出现意外行为。
|
||||
|
||||
例如:
|
||||
|
||||
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
|
||||
for (String s : list) {
|
||||
if (s.equals("b")) {
|
||||
list.remove(s); // 会抛出 ConcurrentModificationException
|
||||
}
|
||||
}
|
||||
|
||||
在上面的代码中,`foreach` 循环遍历 `list` 时,如果删除了元素 `b`,它会修改 `list` 的结构,从而导致 `Iterator` 检测到并发修改,抛出异常。
|
||||
|
||||
### 2. **不可预测的行为**
|
||||
|
||||
即使没有抛出 `ConcurrentModificationException`,在 `foreach` 循环中修改集合也会导致不可预测的行为。例如,`remove` 或 `add` 操作会改变集合的大小和内容,可能会影响迭代的顺序或导致遗漏某些元素,甚至造成死循环或跳过某些元素。
|
||||
|
||||
例如:
|
||||
|
||||
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
|
||||
for (String s : list) {
|
||||
if (s.equals("b")) {
|
||||
list.add("e"); // 修改集合的大小
|
||||
}
|
||||
System.out.println(s);
|
||||
}
|
||||
|
||||
在这个例子中,`add` 操作会向 `list` 中添加一个新元素 `"e"`,从而修改了集合的结构。因为 `foreach` 循环的内部实现使用了迭代器,它可能不会考虑到修改后的新元素,导致输出顺序或遍历结果与预期不同。
|
||||
|
||||
### 3. **迭代器的 `remove()` 方法**
|
||||
|
||||
如果需要在循环中删除元素,推荐使用 `Iterator` 显式地进行删除操作。`Iterator` 提供了一个安全的 `remove()` 方法,可以在遍历时安全地删除元素,而不会引发 `ConcurrentModificationException`。
|
||||
|
||||
例如:
|
||||
|
||||
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
|
||||
Iterator<String> iterator = list.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String s = iterator.next();
|
||||
if (s.equals("b")) {
|
||||
iterator.remove(); // 使用 Iterator 的 remove() 方法
|
||||
}
|
||||
}
|
||||
|
||||
使用 `Iterator.remove()` 可以安全地在遍历时删除元素,而不会抛出并发修改异常。
|
||||
|
||||
### 小结
|
||||
|
||||
在 `foreach` 循环中直接进行 `remove` 或 `add` 操作是不安全的,主要有以下原因:
|
||||
|
||||
- **`ConcurrentModificationException`**:直接修改集合会触发迭代器的并发修改检测,导致异常。
|
||||
- **不可预测的行为**:修改集合的结构可能导致元素遗漏、顺序错乱或程序逻辑出错。
|
||||
- **使用 `Iterator` 替代**:使用 `Iterator` 的 `remove()` 方法可以避免这些问题,实现安全的元素删除操作。
|
||||
|
||||
因此,正确的做法是使用 `Iterator` 显式地处理元素的删除或修改,而不是直接在 `foreach` 循环中进行修改。
|
||||
|
||||
## 10. 为什么禁止工程师直接使用日志系统 (Log4j、Logback) 中的 API ?
|
||||
|
||||
在很多工程实践中,**禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API**,主要是出于以下几个原因:
|
||||
|
||||
### 1. **日志配置与实现的分离**
|
||||
|
||||
直接使用日志系统的 API 可能会导致日志记录逻辑与应用的业务逻辑紧密耦合,使得日志配置和实现的分离变得困难。现代的日志框架(如 Log4j、Logback)允许通过外部配置文件(如 `log4j.xml` 或 `logback.xml`)灵活配置日志级别、输出格式、输出位置等,而不是硬编码到应用代码中。直接使用日志 API 会导致日志的配置与业务代码绑定在一起,不易修改和维护。
|
||||
|
||||
**建议的做法**:通过使用日志框架的日志抽象接口(如 `org.slf4j.Logger`)来记录日志,而不是直接依赖具体的日志实现。这种方式提供了更大的灵活性,日志实现可以在运行时通过配置文件更换而无需修改代码。
|
||||
|
||||
### 2. **灵活性与可扩展性问题**
|
||||
|
||||
如果工程师直接使用日志库的 API,项目在需要切换日志框架(比如从 Log4j 转换到 Logback 或其他框架)时,需要修改大量的代码,增加了系统的耦合度和维护难度。另一方面,使用日志抽象层(如 SLF4J)可以避免这一问题,因为 SLF4J 是一个日志抽象层,底层可以切换具体的日志实现而无需改变业务代码。
|
||||
|
||||
**示例**:
|
||||
|
||||
// 不推荐:直接使用 Log4j 的 API
|
||||
import org.apache.log4j.Logger;
|
||||
Logger logger = Logger.getLogger(MyClass.class);
|
||||
logger.info("This is a log message");
|
||||
|
||||
// 推荐:通过 SLF4J 接口来记录日志
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
Logger logger = LoggerFactory.getLogger(MyClass.class);
|
||||
logger.info("This is a log message");
|
||||
|
||||
使用 SLF4J 可以在不同的环境中灵活切换日志实现,而无需修改代码。
|
||||
|
||||
### 3. **日志记录与调试不一致**
|
||||
|
||||
如果工程师直接使用日志框架的 API,可能会在日志记录时不遵循一致的日志策略。例如,日志的级别、格式、日志输出的内容等可能不统一,导致日志信息混乱、不易追踪。通过统一的日志抽象接口(如 SLF4J)和规范的日志记录策略(通过 AOP 或日志框架自带的特性)可以保持日志的一致性和规范性。
|
||||
|
||||
**最佳实践**:
|
||||
|
||||
- 通过统一的日志管理类或工具类来封装日志记录方法,确保所有日志记录都遵循统一的格式和规范。
|
||||
- 在日志中统一使用适当的日志级别(如 `DEBUG`、`INFO`、`WARN`、`ERROR`)和标准格式。
|
||||
|
||||
### 4. **日志的性能影响**
|
||||
|
||||
日志记录可能对应用的性能产生一定的影响,尤其是在日志记录过于频繁或日志输出内容过多的情况下。通过直接使用日志框架的 API,可能无法灵活控制日志输出的频率、内容或过滤策略,从而造成性能问题。很多日志框架(如 Log4j 和 Logback)提供了高级的配置选项,如异步日志、日志缓存等特性,可以显著提高性能。
|
||||
|
||||
**推荐做法**:
|
||||
|
||||
- 使用日志框架提供的异步日志功能来提高性能。
|
||||
- 配置适当的日志级别,避免在生产环境中输出过多的调试信息。
|
||||
|
||||
### 5. **日志管理的统一性与规范**
|
||||
|
||||
在团队开发中,直接使用日志框架的 API 会导致不同开发人员在不同模块中记录日志时不遵循统一规范,导致日志格式不统一、信息不一致,甚至产生重复的日志记录。通过日志管理工具类或封装类,可以确保所有开发人员遵循统一的日志记录策略。
|
||||
|
||||
**示例**:
|
||||
|
||||
- 创建一个统一的 `LoggerFactory` 工厂类来生成日志记录对象。
|
||||
- 统一定义日志级别和输出格式,确保日志输出一致。
|
||||
|
||||
### 小结
|
||||
|
||||
禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API,主要是为了:
|
||||
|
||||
1. **解耦日志实现与业务逻辑**:通过使用日志抽象层(如 SLF4J),可以更轻松地切换日志框架,避免硬编码。
|
||||
2. **提高灵活性与可维护性**:避免在应用中重复使用框架 API,提高日志配置的灵活性和一致性。
|
||||
3. **规范日志记录行为**:通过封装日志记录,确保日志级别、格式和内容的统一,增强可读性和可追踪性。
|
||||
4. **优化性能**:通过配置日志框架的高级功能(如异步日志),提高日志系统的性能,减少对应用的影响。
|
||||
5. **统一日志管理**:避免团队成员在不同模块中使用不一致的日志记录方式,确保日志输出的标准化。
|
||||
|
||||
最好的做法是通过日志抽象层(如 SLF4J)进行日志记录,同时通过日志管理工具类进行统一的配置和调用,确保日志的高效、规范和灵活性。
|
||||
|
||||
## 11. 为什么建议开发者谨慎使用继承?
|
||||
|
||||
在面向对象编程(OOP)中,**继承**是一种常见的代码复用方式,它允许一个类继承另一个类的属性和行为。然而,虽然继承可以提高代码的复用性,但过度或不当使用继承可能会导致代码的复杂性增加,进而带来一些潜在的问题。因此,建议开发者在使用继承时要谨慎,以下是一些关键原因:
|
||||
|
||||
### 1. **增加了类之间的耦合性**
|
||||
|
||||
继承会导致子类和父类之间形成紧密的耦合关系。子类依赖于父类的实现,这意味着如果父类发生变化,可能会影响到所有继承自该父类的子类,导致修改和维护变得更加困难。这种紧密耦合关系也限制了子类的灵活性,因为它必须遵循父类的接口和实现。
|
||||
|
||||
**例子**:
|
||||
|
||||
class Animal {
|
||||
void eat() {
|
||||
System.out.println("Animal is eating");
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
@Override
|
||||
void eat() {
|
||||
System.out.println("Dog is eating");
|
||||
}
|
||||
}
|
||||
|
||||
如果父类 `Animal` 做了改动(如修改 `eat()` 方法的实现),`Dog` 类也会受到影响。这样的耦合会增加后期维护的复杂度。
|
||||
|
||||
### 2. **破坏了封装性(Encapsulation)**
|
||||
|
||||
继承可能破坏封装性,因为子类可以直接访问父类的成员(字段和方法),尤其是当父类成员被设置为 `protected` 或 `public` 时。这种情况可能导致子类暴露不应被外界访问的细节,破坏了数据的封装性。
|
||||
|
||||
**例子**:
|
||||
|
||||
class Vehicle {
|
||||
protected int speed;
|
||||
}
|
||||
|
||||
class Car extends Vehicle {
|
||||
void accelerate() {
|
||||
speed += 10; // 直接访问父类的 protected 字段
|
||||
}
|
||||
}
|
||||
|
||||
在这种情况下,`Car` 类直接访问了父类 `Vehicle` 的 `speed` 字段,而不是通过公共接口来修改它,导致封装性降低。
|
||||
|
||||
### 3. **继承可能会导致类的层次结构不合理**
|
||||
|
||||
继承往往会导致不合理的类层次结构,特别是在试图通过继承来表达“是一个”(`is-a`)关系时,实际情况可能并不符合这种逻辑。滥用继承可能会使类之间的关系变得复杂和不直观,导致代码结构混乱。
|
||||
|
||||
**例子**:
|
||||
假设我们有一个 `Car` 类和一个 `Truck` 类,都继承自 `Vehicle` 类。如果 `Car` 和 `Truck` 共享很多方法和属性,这样的设计可能是合适的。但是,如果 `Car` 和 `Truck` 之间差异很大,仅通过继承来构建它们的关系,可能会导致继承层次过于复杂,代码阅读和理解变得困难。
|
||||
|
||||
### 4. **继承可能导致不易发现的错误**
|
||||
|
||||
由于子类继承了父类的行为,任何对父类的修改都有可能影响到子类的行为。更糟糕的是,错误或不一致的修改可能在父类中发生,而这些错误可能不会立即暴露出来,直到程序运行到某个特定的地方,才会显现出错误。
|
||||
|
||||
**例子**:
|
||||
假设你修改了父类的某个方法,但忘记更新或调整子类中相应的重写方法,这可能会导致难以发现的错误。
|
||||
|
||||
### 5. **继承限制了灵活性(不可重用性问题)**
|
||||
|
||||
继承创建了一个父类与子类之间的固定关系,这意味着如果你想在一个完全不同的上下文中重用一个类,你可能不能通过继承来实现。在某些情况下,组合比继承更为灵活,允许你将多个行为组合到一个类中,而不是通过继承来强行构建类的层次结构。
|
||||
|
||||
**例子**:
|
||||
|
||||
// 组合而非继承
|
||||
class Engine {
|
||||
void start() {
|
||||
System.out.println("Engine started");
|
||||
}
|
||||
}
|
||||
|
||||
class Car {
|
||||
private Engine engine = new Engine(); // 通过组合来使用 Engine
|
||||
void start() {
|
||||
engine.start();
|
||||
}
|
||||
}
|
||||
|
||||
通过组合,可以灵活地使用不同的组件,而不需要继承整个类。这样做的优点是更具扩展性和灵活性。
|
||||
|
||||
### 6. **继承限制了方法的重用(可维护性差)**
|
||||
|
||||
如果你过度依赖继承,你的代码会容易受到父类实现的限制,难以灵活地添加新功能或进行扩展。例如,在继承链中添加新的功能可能会导致一大堆方法的修改和重写,而不通过继承,可以更轻松地将功能作为独立模块来重用。
|
||||
|
||||
### 7. **使用接口和组合更优**
|
||||
|
||||
相比继承,**接口(Interface)** 和 **组合(Composition)** 更符合面向对象设计的原则。接口允许类只暴露所需的功能,而不暴露实现细节,组合则允许你将多个不同的行为组合在一起,使得系统更加灵活和可扩展。通过接口和组合,可以避免继承的许多问题。
|
||||
|
||||
**推荐设计模式**:
|
||||
|
||||
- **策略模式(Strategy Pattern)**:通过接口和组合来替代继承。
|
||||
- **装饰器模式(Decorator Pattern)**:使用组合和代理来扩展行为,而非通过继承。
|
||||
|
||||
### 小结
|
||||
|
||||
尽管继承是面向对象编程中的一个重要特性,但滥用继承可能带来许多问题,特别是在以下几个方面:
|
||||
|
||||
- 增加类之间的耦合,降低灵活性;
|
||||
- 破坏封装性,暴露不应访问的内部实现;
|
||||
- 可能导致类层次结构复杂,增加理解和维护的难度;
|
||||
- 限制代码的重用和扩展性。
|
||||
|
||||
因此,**推荐优先使用组合而非继承**,并尽可能使用接口来实现灵活的扩展。如果必须使用继承,确保它能够清晰地表达“是一个”的关系,并避免过深的继承层次。
|
||||
|
||||
## 12. 为什么禁止开发人员修改 serialVersionUID 字段的值?
|
||||
|
||||
`serialVersionUID` 是 Java 中用来标识序列化版本的一个静态字段。它的作用是确保在反序列化时,JVM 可以验证序列化的类与当前类的兼容性,以避免版本不兼容导致的错误。尽管 `serialVersionUID` 可以由开发人员手动定义,**禁止开发人员修改 `serialVersionUID` 字段的值** 的原因如下:
|
||||
|
||||
### 1. **序列化与反序列化兼容性**
|
||||
|
||||
`serialVersionUID` 的主要作用是保证在序列化和反序列化过程中,类的版本兼容性。它是用来标识类的版本的,如果序列化和反序列化过程中使用的类的 `serialVersionUID` 不匹配,就会抛出 `InvalidClassException`。
|
||||
|
||||
- **不匹配的 `serialVersionUID`** 会导致序列化的数据与当前类不兼容,导致反序列化失败。
|
||||
- **修改 `serialVersionUID`** 的值会改变类的版本标识,导致已序列化的数据在反序列化时不能成功读取,特别是在类结构发生改变(例如添加或删除字段)时。
|
||||
|
||||
**例如:**
|
||||
|
||||
// 类的第一次版本
|
||||
public class MyClass implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String name;
|
||||
// 其他字段和方法
|
||||
}
|
||||
|
||||
// 类的第二次修改版本
|
||||
public class MyClass implements Serializable {
|
||||
private static final long serialVersionUID = 2L; // 修改了 serialVersionUID
|
||||
private String name;
|
||||
private int age; // 新增字段
|
||||
// 其他字段和方法
|
||||
}
|
||||
|
||||
如果修改了 `serialVersionUID`,而之前序列化的数据是使用版本 1 的类进行序列化的,反序列化时会因为 `serialVersionUID` 不匹配而导致失败。
|
||||
|
||||
### 2. **避免不必要的版本冲突**
|
||||
|
||||
Java 会根据类的字段、方法等信息自动生成 `serialVersionUID`,这个值是基于类的结构计算出来的。如果开发人员修改了 `serialVersionUID`,可能会破坏 Java 自动生成的版本控制机制,从而导致版本控制不一致,增加了维护复杂性。
|
||||
|
||||
如果手动修改 `serialVersionUID`,容易出现以下几种问题:
|
||||
|
||||
- 由于类结构没有变化,修改 `serialVersionUID` 可能会导致已序列化的数据无法恢复。
|
||||
- 如果不同的开发人员修改了 `serialVersionUID`,可能会在不同的机器或系统间引起序列化不一致。
|
||||
|
||||
### 3. **影响序列化兼容性**
|
||||
|
||||
Java 提供了两种主要的兼容性规则:
|
||||
|
||||
- **兼容性向前**:如果类的字段或方法发生改变,但没有改变 `serialVersionUID`,则反序列化是可以工作的。
|
||||
- **兼容性向后**:如果你修改了类的结构(如字段变动、方法签名改变等),并且保持相同的 `serialVersionUID`,反序列化仍然可以工作。
|
||||
|
||||
如果不小心修改了 `serialVersionUID`,可能导致以下情况:
|
||||
|
||||
- **向前兼容性**:新版本的类不能兼容老版本的对象,导致反序列化失败。
|
||||
- **向后兼容性**:老版本的类无法反序列化新版本的对象。
|
||||
|
||||
### 4. **自动生成 vs 手动指定**
|
||||
|
||||
- **自动生成的 `serialVersionUID`**:Java 会根据类的结构自动生成 `serialVersionUID`,这样如果类的结构发生变化,`serialVersionUID` 会自动变化,确保不兼容的版本之间不会出现意外的反序列化行为。
|
||||
- **手动指定 `serialVersionUID`**:手动修改 `serialVersionUID` 可能导致版本控制不一致,特别是在多人开发、分布式部署的环境中,容易出现反序列化失败的问题。
|
||||
|
||||
### 5. **避免非预期的反序列化问题**
|
||||
|
||||
手动修改 `serialVersionUID` 可能会导致数据丢失或反序列化时抛出异常。例如,如果开发人员错误地修改了 `serialVersionUID`,系统在尝试反序列化时可能会因为 `serialVersionUID` 不匹配而无法成功加载对象,导致异常的发生。
|
||||
|
||||
### 小结
|
||||
|
||||
**禁止开发人员修改 `serialVersionUID` 字段的值,主要是为了:**
|
||||
|
||||
- **确保序列化与反序列化的兼容性**,避免版本不匹配导致反序列化失败。
|
||||
- **避免不必要的版本冲突和数据丢失**,特别是在类结构修改时。
|
||||
- **保持 Java 自动管理 `serialVersionUID` 的优势**,保证类的版本一致性和可维护性。
|
||||
|
||||
如果确实需要修改 `serialVersionUID`,应确保修改后的版本与已经序列化的数据兼容,并遵循合理的版本管理策略。
|
||||
|
||||
## 13. 为什么禁止开发人员使用 isSuccess 作为变量名?
|
||||
|
||||
禁止开发人员使用 `isSuccess` 作为变量名,主要是为了遵循更好的编程规范和提高代码的可读性、可维护性。这个变量名问题的核心在于其容易引起歧义和混淆。具体原因如下:
|
||||
|
||||
### 1. **不符合布尔值命名约定**
|
||||
|
||||
在 Java 中,通常使用 `is` 或 `has` 开头的变量名来表示布尔值(`boolean` 类型)。这类命名通常遵循特定的语义约定,表示某个条件是否成立。例如:
|
||||
|
||||
- `isEnabled` 表示某个功能是否启用;
|
||||
- `hasPermission` 表示是否有权限。
|
||||
|
||||
**问题**:
|
||||
|
||||
- `isSuccess` 看起来像一个布尔值(`boolean` 类型),但它实际上可能并不直接表示一个布尔值,而是一个状态或结果。这种命名可能会导致混淆,开发者可能误以为它是布尔类型的变量,而实际上它可能是一个描述状态的对象、字符串或者其他类型的数据。
|
||||
|
||||
### 2. **语义不明确**
|
||||
|
||||
`isSuccess` 这个名字表面上表示“是否成功”,但是它缺少具体的上下文,导致语义不够明确。真正表示是否成功的布尔值应该直接使用 `boolean` 类型的变量,并且使用清晰明确的命名。
|
||||
|
||||
例如:
|
||||
|
||||
- `isCompleted`:表示某个任务是否完成。
|
||||
- `isSuccessful`:表示某个操作是否成功。
|
||||
|
||||
这些命名能更明确地表达布尔变量的含义,避免理解上的歧义。
|
||||
|
||||
### 3. **与标准的 `is` 前缀混淆**
|
||||
|
||||
`is` 前缀通常用来表示“是否”某个条件成立,适用于返回布尔值的方法或者变量。`isSuccess` 这样的命名会让开发人员误以为它是一个布尔值,或者一个 `boolean` 类型的值,但实际上它可能是一个复杂类型或者其他非布尔类型,造成不必要的混淆。
|
||||
|
||||
例如:
|
||||
|
||||
boolean isSuccess = someMethod(); // 看起来是布尔值,但实际类型可能不同
|
||||
|
||||
这种情况可能导致开发人员产生误解,认为 `isSuccess` 代表的是布尔值,但它可能是某个表示成功的对象、枚举或者其他数据类型。
|
||||
|
||||
### 4. **更好的命名建议**
|
||||
|
||||
为了避免歧义和混淆,开发人员应使用更加明确且符合命名规范的名称。以下是一些命名的改进建议:
|
||||
|
||||
- 如果是布尔值,命名为 `isSuccessful` 或 `wasSuccessful`。
|
||||
- 如果是表示结果的对象,使用更具体的名称,例如 `operationResult` 或 `statusCode`,以表明它是一个描述操作结果的变量。
|
||||
|
||||
### 5. **提升代码的可读性和可维护性**
|
||||
|
||||
清晰且具有意义的命名能够帮助团队成员或未来的开发者更快地理解代码的意图。如果变量名过于模糊(如 `isSuccess`),就可能让人对其实际含义产生疑问,尤其是在阅读较大或复杂的代码时。良好的命名能够提升代码的可读性和可维护性。
|
||||
|
||||
### 小结
|
||||
|
||||
- **`isSuccess`** 这样的命名不清晰,容易与布尔类型的变量产生混淆,进而影响代码的可读性。
|
||||
- **命名应尽量明确**,避免使用容易引起歧义的名称,特别是在布尔值类型的命名时。
|
||||
- 建议使用更具描述性的名称,如 **`isSuccessful`** 或 **`wasSuccessful`**,更清晰地表达变量的意义。
|
||||
|
||||
## 最后
|
||||
|
||||
以上是 V 哥精心总结的13个 Java 编程中的小小编码问题,也是V 哥日常编码中总结的学习笔记,分享给大家,如果内容对你有帮助,请不要吝啬来个小赞呗,关注威哥爱编程,Java 路上,你我相伴前行。
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
###### 1. Integer[]转[ArrayList](https://so.csdn.net/so/search?q=ArrayList&spm=1001.2101.3001.7020)
|
||||
|
||||
(1) 方法一:
|
||||
|
||||
利用[Arrays工具类](https://blog.csdn.net/k909397116/article/details/107308897)中的`asList`方法
|
||||
|
||||
```Java
|
||||
Integer[] arr = {1,2,3};
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(arr));
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
(2) 方法二:
|
||||
|
||||
利用Collections工具类中的`addAll`方法
|
||||
|
||||
```Java
|
||||
Integer[] arr = {1,2,3};
|
||||
ArrayList<Integer> list = new ArrayList<>(array.length);
|
||||
Collections.addAll(list, arr);
|
||||
|
||||
|
||||
|
||||
123
|
||||
123
|
||||
```
|
||||
|
||||
(3) 注意:
|
||||
Java中集合只能存放引用数据类型,在使用`asList`或`addAll`方法时,被转换的[数组](https://so.csdn.net/so/search?q=%E6%95%B0%E7%BB%84&spm=1001.2101.3001.7020)必须是存放引用数据类型的数组,如果是基本数据类型数组请在转换前先把其转换为对应的[包装类型](https://blog.csdn.net/k909397116/article/details/107323121)数组,下面会介绍。
|
||||
|
||||
|
||||
|
||||
###### 2. ArrayList转Integer[]
|
||||
|
||||
```Java
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
Integer[] arr = list.toArray(new Integer[0]);
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 二、Integer[]与int[]互转
|
||||
|
||||
---
|
||||
|
||||
###### 1. Integer[]转int[]
|
||||
|
||||
```Java
|
||||
Integer[] arr1 = {1,2,3};
|
||||
int[] arr2 = Arrays.stream(arr1).mapToInt(Integer::valueOf).toArray();
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
###### 2. int[]转Integer[]
|
||||
|
||||
```Java
|
||||
int[] arr1 = {1,2,3};
|
||||
Integer[] arr2 = Arrays.stream(arr1).boxed().toArray(Integer[]::new);
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 三、int[]与ArrayList的互转
|
||||
|
||||
---
|
||||
|
||||
###### 1. int[]转ArrayList
|
||||
|
||||
```Java
|
||||
int[] arr = {1,2,3};
|
||||
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
###### 2. ArrayList转int[]
|
||||
|
||||
```Java
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
int[] arr = list.stream().mapToInt(Integer::valueOf).toArray();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
###### 1. Integer[]转[ArrayList](https://so.csdn.net/so/search?q=ArrayList&spm=1001.2101.3001.7020)
|
||||
|
||||
(1) 方法一:
|
||||
|
||||
利用[Arrays工具类](https://blog.csdn.net/k909397116/article/details/107308897)中的`asList`方法
|
||||
|
||||
```Java
|
||||
Integer[] arr = {1,2,3};
|
||||
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(arr));
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
(2) 方法二:
|
||||
|
||||
利用Collections工具类中的`addAll`方法
|
||||
|
||||
```Java
|
||||
Integer[] arr = {1,2,3};
|
||||
ArrayList<Integer> list = new ArrayList<>(array.length);
|
||||
Collections.addAll(list, arr);
|
||||
|
||||
|
||||
|
||||
123
|
||||
123
|
||||
```
|
||||
|
||||
(3) 注意:
|
||||
Java中集合只能存放引用数据类型,在使用`asList`或`addAll`方法时,被转换的[数组](https://so.csdn.net/so/search?q=%E6%95%B0%E7%BB%84&spm=1001.2101.3001.7020)必须是存放引用数据类型的数组,如果是基本数据类型数组请在转换前先把其转换为对应的[包装类型](https://blog.csdn.net/k909397116/article/details/107323121)数组,下面会介绍。
|
||||
|
||||
|
||||
|
||||
###### 2. ArrayList转Integer[]
|
||||
|
||||
```Java
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
Integer[] arr = list.toArray(new Integer[0]);
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 二、Integer[]与int[]互转
|
||||
|
||||
---
|
||||
|
||||
###### 1. Integer[]转int[]
|
||||
|
||||
```Java
|
||||
Integer[] arr1 = {1,2,3};
|
||||
int[] arr2 = Arrays.stream(arr1).mapToInt(Integer::valueOf).toArray();
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
###### 2. int[]转Integer[]
|
||||
|
||||
```Java
|
||||
int[] arr1 = {1,2,3};
|
||||
Integer[] arr2 = Arrays.stream(arr1).boxed().toArray(Integer[]::new);
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 三、int[]与ArrayList的互转
|
||||
|
||||
---
|
||||
|
||||
###### 1. int[]转ArrayList
|
||||
|
||||
```Java
|
||||
int[] arr = {1,2,3};
|
||||
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());
|
||||
|
||||
|
||||
|
||||
12
|
||||
12
|
||||
```
|
||||
|
||||
|
||||
|
||||
###### 2. ArrayList转int[]
|
||||
|
||||
```Java
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
int[] arr = list.stream().mapToInt(Integer::valueOf).toArray();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
## Maven 依赖
|
||||
|
||||
```Plain Text
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```Plain Text
|
||||
<dependency>
|
||||
<groupId>com.github.houbb</groupId>
|
||||
<artifactId>junitperf</artifactId>
|
||||
<version>1.0.3</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Maven 插件
|
||||
|
||||
```Plain Text
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>racal.sunyard.main.Version</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
|
||||
|
||||
```Plain Text
|
||||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>fat-tests</id>
|
||||
<formats>
|
||||
<format>jar</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<dependencySets>
|
||||
<dependencySet>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<useProjectArtifact>true</useProjectArtifact>
|
||||
<unpack>true</unpack>
|
||||
<scope>test</scope>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}/test-classes</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*.class</include>
|
||||
</includes>
|
||||
<useDefaultExcludes>true</useDefaultExcludes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 使用
|
||||
|
||||
使用 `xxx-fat-tests.jar` 测试类 `TestCIPSPref0`
|
||||
|
||||
|
||||
|
||||
执行脚本
|
||||
|
||||
```Plain Text
|
||||
timeout /nobreak /t 5
|
||||
C:\j2sdk-image\bin\java -Dip=192.168.5.8 -Dfile.encoding=utf-8 -cp .\xxx-fat-tests.jar org.junit.runner.JUnitCore TestCIPSPref0
|
||||
@pause
|
||||
```
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
## Maven 依赖
|
||||
|
||||
```Plain Text
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```Plain Text
|
||||
<dependency>
|
||||
<groupId>com.github.houbb</groupId>
|
||||
<artifactId>junitperf</artifactId>
|
||||
<version>1.0.3</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Maven 插件
|
||||
|
||||
```Plain Text
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>racal.sunyard.main.Version</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
|
||||
|
||||
```Plain Text
|
||||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>fat-tests</id>
|
||||
<formats>
|
||||
<format>jar</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<dependencySets>
|
||||
<dependencySet>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<useProjectArtifact>true</useProjectArtifact>
|
||||
<unpack>true</unpack>
|
||||
<scope>test</scope>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}/test-classes</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*.class</include>
|
||||
</includes>
|
||||
<useDefaultExcludes>true</useDefaultExcludes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 使用
|
||||
|
||||
使用 `xxx-fat-tests.jar` 测试类 `TestCIPSPref0`
|
||||
|
||||
|
||||
|
||||
执行脚本
|
||||
|
||||
```Plain Text
|
||||
timeout /nobreak /t 5
|
||||
C:\j2sdk-image\bin\java -Dip=192.168.5.8 -Dfile.encoding=utf-8 -cp .\xxx-fat-tests.jar org.junit.runner.JUnitCore TestCIPSPref0
|
||||
@pause
|
||||
```
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
### 前言
|
||||
|
||||
> 最近几天从零接触LSPosed(xposed),需要自己开发模块进行使用,做一下学习笔记。
|
||||
>
|
||||
> 本篇内容在已经root并且成功刷入了Magisk的基础上进行。
|
||||
>
|
||||
> 注意:LSPosed只支持安卓版本8.x+的设备。如果在8.0以下需要使用xposed。他们的模块开发接口也是一样的。
|
||||
|
||||
|
||||
### 什么是Magisk
|
||||
|
||||
> 我理解它是需要[root权限](https://so.csdn.net/so/search?q=root%E6%9D%83%E9%99%90&spm=1001.2101.3001.7020)的工具类软件的载体,通过它来获取root权限(现在的安卓手机都不能直接获取root权限),同时也起到一种“隐身”的作用,防止被应用程序知道被有root权限的工具操作了。(有用过手游外挂或者gg修改器的人应该懂什么意思🤪)
|
||||
>
|
||||
> 引用AI的回答:
|
||||
>
|
||||
> Magisk是一个非常流行的Android系统修改工具,由XDA开发者开发。它的核心功能是提供一个系统级的界面,允许用户在不改变系统分区的情况下,以模块化的方式对Android系统进行修改和定制。Magisk的主要特点和作用包括:
|
||||
>
|
||||
> 系统分区保护:Magisk通过在系统启动过程中注入代码,实现了对系统分区的无损修改。这意味着用户可以在享受定制化功能的同时,保持系统的纯净状态,便于官方系统更新。
|
||||
>
|
||||
> Root权限管理:Magisk提供了一个灵活的SU(SuperUser)管理机制,允许用户控制哪些应用程序可以获得root权限,提高了安全性。
|
||||
>
|
||||
> 模块支持:Magisk的一个关键特性是支持各种模块,这些模块可以是功能增强、界面美化、系统优化等。用户可以根据需求安装不同的Magisk模块来扩展或修改系统功能,LSPosed就是其中之一。
|
||||
>
|
||||
> 隐藏特性:Magisk还具有“隐身”功能,能够隐藏系统被修改的事实,这对于某些需要安全验证的应用(如银行应用)特别有用。
|
||||
>
|
||||
> 至于为什么LSPosed需要使用Magisk,这是因为:
|
||||
>
|
||||
> 安全性和便利性:Magisk为LSPosed这样的系统级修改框架提供了一个安全可靠的安装环境。通过Magisk管理器,用户可以轻松安装、更新或卸载LSPosed,而不会影响到系统的完整性。
|
||||
> 权限管理:LSPosed需要root权限来执行其核心的Hook功能,Magisk提供的SU管理机制正好满足了这一需求,同时允许用户精细控制权限分配,确保安全。
|
||||
>
|
||||
> 模块化集成:作为Magisk的一个模块,LSPosed可以无缝集成到Magisk生态系统中,与其他模块共存,共同为用户提供一个高度可定制的Android体验。
|
||||
|
||||
|
||||
### 什么是LSPosed(xposed)
|
||||
|
||||
> 简单说就是可以在不修改应用程序的情况下Hook应用程序。类似于使用frida进行hook app。LSPosed的作用就是你可以自己开发一个安卓Hook应用,然后使用LSPosed加载它,并且指定对某个app进行hook。
|
||||
>
|
||||
> 引用AI回答:
|
||||
>
|
||||
> LSPosed是一种针对Android系统的高级定制和修改框架,它是Edxposed的一个分支项目。LSPosed的设计目标是在不修改任何应用和系统组件的情况下,通过hook技术实现在运行时修改应用程序行为的能力,类似于知名的Xposed框架。这一框架为Android用户和开发者提供了强大的功能,允许他们定制系统和应用程序,比如修改系统设置、视觉效果、或者增强应用功能等,而无需触及原始的应用程序代码或重新编译系统。
|
||||
>
|
||||
> LSPosed的关键特性包括:
|
||||
>
|
||||
> 基于Riru的ART Hook框架:LSPosed建立在Riru的基础上,Riru是一个低层次的Hook框架,能够在Android系统的ART(Android Runtime)层面上工作,支持从Android 8.0到11的版本。
|
||||
>
|
||||
> 与Xposed兼容的API:它提供了与原版Xposed相同的API,这意味着许多为Xposed开发的模块可以直接在LSPosed上运行,降低了迁移成本。
|
||||
>
|
||||
> 更高的效率和稳定性:相比其前身Edxposed,LSPosed被设计得更加简洁、高效且稳定,减少了潜在的崩溃和性能问题。
|
||||
>
|
||||
> 长期更新和支持:由于Edxposed面临着停更的风险和稳定性问题,LSPosed作为一个活跃维护的项目,承诺提供长期的更新支持,不断集成新功能并保持与最新Android版本的兼容性。
|
||||
> 使用YAHFA或SandHook进行Hook:这两种技术帮助LSPosed在应用启动时动态插入代码,实现无痕且高效的Hook操作,无需重启手机即可使Hook模块生效。
|
||||
>
|
||||
> 对于希望深度定制自己Android设备体验的高级用户和开发者而言,LSPosed是一个强大且灵活的工具。不过,使用此类框架通常需要root权限,并且涉及系统级别的修改,因此也伴随着一定的风险和挑战。
|
||||
|
||||
private static void HookLocation(ClassLoader classLoader) throws ClassNotFoundException {
|
||||
//坐标拾取系统 [https://lbs.qq.com/getPoint/](https://lbs.qq.com/getPoint/)
|
||||
double latitude = 0.0;
|
||||
double longitude = 0.0;
|
||||
|
||||
Class clazz = classLoader.loadClass(
|
||||
"com.tencent.map.geolocation.sapp.TencentLocationManager");
|
||||
XposedHelpers.findAndHookMethod(clazz, "requestLocationUpdates",
|
||||
"com.tencent.map.geolocation.sapp.TencentLocationRequest",
|
||||
"com.tencent.map.geolocation.sapp.TencentLocationListener", new XC_MethodHook() {
|
||||
@[Override](https://www.v2ex.com/member/Override)
|
||||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
|
||||
XposedBridge.log("requestLocationUpdates");
|
||||
Class tencentLocationListenerClass = param.args[1].getClass();
|
||||
XposedHelpers.findAndHookMethod(tencentLocationListenerClass,
|
||||
"onLocationChanged",
|
||||
"com.tencent.map.geolocation.sapp.TencentLocation",
|
||||
int.class, String.class, new XC_MethodHook() {
|
||||
@[Override](https://www.v2ex.com/member/Override)
|
||||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
|
||||
Class tencentLocation = param.args[0].getClass();
|
||||
XposedHelpers.findAndHookMethod(tencentLocation,
|
||||
"getLatitude",
|
||||
new XC_MethodHook() {
|
||||
@[Override](https://www.v2ex.com/member/Override)
|
||||
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
|
||||
XposedBridge.log("requestLocationUpdates.getLatitude:"+ param.getResult());
|
||||
param.setResult(latitude);
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(tencentLocation,
|
||||
"getLongitude",
|
||||
new XC_MethodHook() {
|
||||
@[Override](https://www.v2ex.com/member/Override)
|
||||
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
|
||||
XposedBridge.log("requestLocationUpdates.getLongitude:"+ param.getResult());
|
||||
param.setResult(longitude);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
想把[maven](https://so.csdn.net/so/search?q=maven&spm=1001.2101.3001.7020)项目打包成可执行jar包,按照网上使用maven-assembly-plugin配置,打包后,运行报错。
|
||||
报错信息:
|
||||
java.lang.IllegalArgumentException:No auto [configuration](https://so.csdn.net/so/search?q=configuration&spm=1001.2101.3001.7020) classes found in META-INF/spring.factories.If you are using a custom packaging,make sure that file is correct.
|
||||
|
||||
一、
|
||||
|
||||
解决办法:
|
||||
我是springboot项目,直接使用spring-boot-maven-plugin,删除了maven-assembly-plugin配置
|
||||
配置信息如下
|
||||
|
||||
```Plain Text
|
||||
<properties>
|
||||
<start-class>com.xxx.yyy.MainClass</start-class>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<mainClass>${start-class}</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
配置好后,在项目目录下执行 `mvn clean package`
|
||||
执行完成后,cd 进入target目录,运行jar(jar中包含了所有依赖)
|
||||
`java -jar my-service-0.0.1-SNAPSHOT.jar`
|
||||
|
||||
|
||||
|
||||
二、
|
||||
|
||||
### **修改前的pom配置**
|
||||
|
||||
```Plain Text
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>org.task</groupId>
|
||||
<artifactId>task-server-api</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<!-- 文件拷贝时的编码 -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<!-- 编译时的编码 -->
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
......
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<!--这里要替换成jar包main方法所在类 -->
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Class-Path>.</Class-Path>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||
<phase>package</phase> <!-- 指定在打包节点执行jar包合并操作 -->
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
</project>
|
||||
```
|
||||
|
||||
|
||||
|
||||
### **新增如下插件**
|
||||
|
||||
```Plain Text
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
### **修改后**
|
||||
|
||||
```Plain Text
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>org.task</groupId>
|
||||
<artifactId>task-server-api</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<!-- 文件拷贝时的编码 -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<!-- 编译时的编码 -->
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
......
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<!--这里要替换成jar包main方法所在类 -->
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Class-Path>.</Class-Path>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||
<phase>package</phase> <!-- 指定在打包节点执行jar包合并操作 -->
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
```
|
||||
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
想把[maven](https://so.csdn.net/so/search?q=maven&spm=1001.2101.3001.7020)项目打包成可执行jar包,按照网上使用maven-assembly-plugin配置,打包后,运行报错。
|
||||
报错信息:
|
||||
java.lang.IllegalArgumentException:No auto [configuration](https://so.csdn.net/so/search?q=configuration&spm=1001.2101.3001.7020) classes found in META-INF/spring.factories.If you are using a custom packaging,make sure that file is correct.
|
||||
|
||||
一、
|
||||
|
||||
解决办法:
|
||||
我是springboot项目,直接使用spring-boot-maven-plugin,删除了maven-assembly-plugin配置
|
||||
配置信息如下
|
||||
|
||||
```Plain Text
|
||||
<properties>
|
||||
<start-class>com.xxx.yyy.MainClass</start-class>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<mainClass>${start-class}</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
配置好后,在项目目录下执行 `mvn clean package`
|
||||
执行完成后,cd 进入target目录,运行jar(jar中包含了所有依赖)
|
||||
`java -jar my-service-0.0.1-SNAPSHOT.jar`
|
||||
|
||||
|
||||
|
||||
二、
|
||||
|
||||
### **修改前的pom配置**
|
||||
|
||||
```Plain Text
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>org.task</groupId>
|
||||
<artifactId>task-server-api</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<!-- 文件拷贝时的编码 -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<!-- 编译时的编码 -->
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
......
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<!--这里要替换成jar包main方法所在类 -->
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Class-Path>.</Class-Path>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||
<phase>package</phase> <!-- 指定在打包节点执行jar包合并操作 -->
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
</project>
|
||||
```
|
||||
|
||||
|
||||
|
||||
### **新增如下插件**
|
||||
|
||||
```Plain Text
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
### **修改后**
|
||||
|
||||
```Plain Text
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>org.task</groupId>
|
||||
<artifactId>task-server-api</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<!-- 文件拷贝时的编码 -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<!-- 编译时的编码 -->
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
......
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<!--这里要替换成jar包main方法所在类 -->
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Class-Path>.</Class-Path>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||
<phase>package</phase> <!-- 指定在打包节点执行jar包合并操作 -->
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<mainClass>com.task.TaskApplicationStart</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
`ReentrantLock` 是 Java 并发包中提供的可重入锁,比 `synchronized` 更灵活。以下是其常见用法:
|
||||
|
||||
## 1. 基础锁使用
|
||||
|
||||
```java
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class BasicUsage {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private int count = 0;
|
||||
|
||||
public void increment() {
|
||||
lock.lock(); // 获取锁
|
||||
try {
|
||||
count++;
|
||||
System.out.println("Count: " + count);
|
||||
} finally {
|
||||
lock.unlock(); // 必须放在 finally 中确保释放锁
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 尝试获取锁(非阻塞)
|
||||
|
||||
```java
|
||||
public class TryLockExample {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public void tryLockMethod() {
|
||||
if (lock.tryLock()) { // 尝试获取锁,立即返回结果
|
||||
try {
|
||||
// 获取锁成功,执行临界区代码
|
||||
System.out.println("获取锁成功");
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
} else {
|
||||
// 获取锁失败,执行其他逻辑
|
||||
System.out.println("获取锁失败,执行其他操作");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 带超时的尝试获取锁
|
||||
|
||||
```java
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TimeoutLockExample {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public void timeoutLockMethod() throws InterruptedException {
|
||||
// 尝试在指定时间内获取锁
|
||||
if (lock.tryLock(1, TimeUnit.SECONDS)) {
|
||||
try {
|
||||
System.out.println("在1秒内获取到锁");
|
||||
Thread.sleep(2000); // 模拟业务操作
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
} else {
|
||||
System.out.println("获取锁超时");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 可中断的锁获取
|
||||
|
||||
```java
|
||||
public class InterruptibleLockExample {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public void interruptibleMethod() throws InterruptedException {
|
||||
lock.lockInterruptibly(); // 可响应中断的锁获取
|
||||
try {
|
||||
// 长时间操作
|
||||
while (true) {
|
||||
// 模拟长时间任务
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 公平锁与非公平锁
|
||||
|
||||
```java
|
||||
public class FairLockExample {
|
||||
// 创建公平锁(按申请顺序获取锁)
|
||||
private final ReentrantLock fairLock = new ReentrantLock(true);
|
||||
|
||||
// 创建非公平锁(默认,性能更高)
|
||||
private final ReentrantLock unfairLock = new ReentrantLock();
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 条件变量(Condition)
|
||||
|
||||
```java
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class ConditionExample {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Condition condition = lock.newCondition();
|
||||
private boolean conditionMet = false;
|
||||
|
||||
public void awaitCondition() throws InterruptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
while (!conditionMet) {
|
||||
condition.await(); // 释放锁并等待
|
||||
}
|
||||
// 条件满足,执行后续操作
|
||||
System.out.println("条件满足,继续执行");
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void signalCondition() {
|
||||
lock.lock();
|
||||
try {
|
||||
conditionMet = true;
|
||||
condition.signalAll(); // 唤醒所有等待线程
|
||||
// 或使用 condition.signal() 唤醒一个线程
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 锁的查询方法
|
||||
|
||||
```java
|
||||
public class LockStatusExample {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public void checkLockStatus() {
|
||||
System.out.println("是否被当前线程持有: " + lock.isHeldByCurrentThread());
|
||||
System.out.println("是否被锁定: " + lock.isLocked());
|
||||
System.out.println("等待队列长度: " + lock.getQueueLength());
|
||||
System.out.println("当前线程持有锁的次数: " + lock.getHoldCount());
|
||||
|
||||
// 获取锁信息
|
||||
if (lock.isLocked()) {
|
||||
System.out.println("持有锁的线程: " +
|
||||
(lock.getOwner() != null ? lock.getOwner().getName() : "null"));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 综合示例:线程安全的队列
|
||||
|
||||
```java
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class BlockingQueue<T> {
|
||||
private final Queue<T> queue = new LinkedList<>();
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Condition notEmpty = lock.newCondition();
|
||||
private final Condition notFull = lock.newCondition();
|
||||
private final int capacity;
|
||||
|
||||
public BlockingQueue(int capacity) {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public void put(T item) throws InterruptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
while (queue.size() == capacity) {
|
||||
notFull.await(); // 队列满,等待
|
||||
}
|
||||
queue.offer(item);
|
||||
notEmpty.signal(); // 唤醒等待获取的线程
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public T take() throws InterruptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
while (queue.isEmpty()) {
|
||||
notEmpty.await(); // 队列空,等待
|
||||
}
|
||||
T item = queue.poll();
|
||||
notFull.signal(); // 唤醒等待放入的线程
|
||||
return item;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
lock.lock();
|
||||
try {
|
||||
return queue.size();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 与 synchronized 对比
|
||||
|
||||
```java
|
||||
public class Comparison {
|
||||
// synchronized 方式
|
||||
public synchronized void syncMethod() {
|
||||
// 临界区代码
|
||||
}
|
||||
|
||||
// ReentrantLock 方式
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public void lockMethod() {
|
||||
lock.lock();
|
||||
try {
|
||||
// 临界区代码
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **始终在 finally 块中释放锁**,防止异常导致锁无法释放
|
||||
2. **不要嵌套过多 tryLock**,避免代码复杂度增加
|
||||
3. **公平锁适用于等待时间差异大的场景**,但性能较低
|
||||
4. **考虑使用 tryLock 避免死锁**
|
||||
5. **合理使用 Condition 替代 Object 的 wait/notify**
|
||||
|
||||
## 选择建议
|
||||
|
||||
- 简单同步场景:优先使用 `synchronized`
|
||||
- 需要高级功能(超时、可中断、公平锁):使用 `ReentrantLock`
|
||||
- 需要多个等待条件:使用 `Condition`
|
||||
- 性能敏感场景:测试后选择(通常 `synchronized` 性能更好)
|
||||
@@ -0,0 +1,10 @@
|
||||
- @Controller - 用于 Spring MVC 项目中的控制器类。
|
||||
- @Service - 用于服务类。
|
||||
- @RequestMapping - 用于在控制器处理程序方法中配置 URI 映射。
|
||||
- @ResponseBody - 用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。
|
||||
- @PathVariable - 用于将动态值从 URI 映射到处理程序方法参数。
|
||||
- @Autowired - 用于在 spring bean 中自动装配依赖项。
|
||||
- @Qualifier - 使用 @Autowired 注解,以避免在存在多个 bean 类型实例时出现混淆。
|
||||
- @Scope - 用于配置 spring bean 的范围。
|
||||
- @Configuration,@ComponentScan 和 @Bean - 用于基于 java 的配置。
|
||||
- @Aspect,@Before,@After,@Around,@Pointcut - 用于切面编程(AOP)。
|
||||
@@ -0,0 +1,104 @@
|
||||
#### **实现方案**
|
||||
|
||||
实现这个很简单,只需要完成下面两步:
|
||||
|
||||
1. 想办法将 Class 加载到 JVM
|
||||
|
||||
2. 使 Spring 扫描到 Class
|
||||
|
||||
|
||||
有时候java中的各种框架以及复杂的概念源码给我们造成了很大理解障碍,特别是Spring家族的所以先想清楚大致实现步骤比较好
|
||||
|
||||
#### **将Class加载到JVM**
|
||||
|
||||
现在我们设定需要加载的 Jar 在 `/user/local/java/plugins` 目录下,对于第 1 步,现在有如下几种实现方式:
|
||||
|
||||
1. 扩大 `-cp` 由 `AppClassLoader` 帮我们加载外部 Jar
|
||||
|
||||
2. 使用 SpringBoot 提供的启动参数 `loader.path` 实现
|
||||
|
||||
3. 自定义 `classloader` 实现加载指定位置的 Jar
|
||||
|
||||
|
||||
其中 1 和 3 是JDK提供的方案,2是 SpringBoot 扩展的方案,毕竟 SpringBoot 也需要自定义加载很多类顺带扩展了一个参数出来
|
||||
|
||||
##### **方案1:扩大 -cp 由 AppClassLoader 帮我们加载外部 Jar**
|
||||
|
||||
可以使用 `classpath` 指定类加载的路径,但 `classpath` 的生效是有条件的,这里其实对应了 Jar 包的两种启动方式:通过Jar包里的Main函数启动或者运行Jar启动
|
||||
|
||||

|
||||
|
||||
图片
|
||||
|
||||
SpringBoot 打包后 `MANIFEST.MF` 内容如下:
|
||||
|
||||
`Manifest-Version: 1.0 Created-By: Maven JAR Plugin 3.2.2 Build-Jdk-Spec: 18 Implementation-Title: intelligent-soul Implementation-Version: 0.0.1-SNAPSHOT Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.test.MyApplication Spring-Boot-Version: 2.6.15 Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Spring-Boot-Layers-Index: BOOT-INF/layers.idx `
|
||||
|
||||
所以需要使用 Main 函数启动的方式,不能使用之前的常用的 Jar 包启动方式了。对于 SpringBoot 应用 `Main-Class` 是 `JarLauncher` 所以此时启动命令要改成
|
||||
|
||||
`java -cp /user/local/java/plugins org.springframework.boot.loader.JarLauncher `
|
||||
|
||||
- 优点:实现简单
|
||||
|
||||
- 缺点:每个 Jar 包启动命令都一样了,不好区分
|
||||
|
||||
|
||||
##### **方案2:使用SpringBoot扩展的启动参数,使用java -jar 配合Loader.path参数**
|
||||
|
||||
因为 Spring Boot 程序大多是打成 Jar 包,使用 `java -jar boot.jar` 的方式启动 (此时 `-cp` 无效),可以使用 `loader.path` 指定类加载路径加载其他 Jar,但 `loader.path` 生效也是有条件的:
|
||||
|
||||

|
||||
|
||||
图片
|
||||
|
||||
SpringBoot 默认打包方式的`Main-Class`是`JarLauncher`
|
||||
|
||||
**配置 Main-Class**
|
||||
|
||||
为了使 `loader.path` 参数生效需要生成 Jar 包的 `Main-Class` 项配置为 `PropertiesLauncher`,在 `maven` 中如下配置,可参考 `Using the PropertiesLauncher`:
|
||||
|
||||
`<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!-- 打包时排除scope为system的包 --> <includeSystemScope>false</includeSystemScope> <!-- jvm启动时通过-Dloader.path加载包,必须指定layout为ZIP,否则-Dloader.path无效!!! --> <layout>ZIP</layout> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> <finalName>Test</finalName> </build> `
|
||||
|
||||
其中 layout 可配置值如下:
|
||||
|
||||
1、**JAR**,即通常的可执行 jar
|
||||
|
||||
`Main-Class: org.springframework.boot.loader.JarLauncher `
|
||||
|
||||
2、**WAR**,即通常的可执行war,需要的 servlet 容器依赖位于 `WEB-INF/lib-provided`
|
||||
|
||||
`Main-Class: org.springframework.boot.loader.WarLauncher `
|
||||
|
||||
3、**ZIP**,即DIR,类似于 JAR
|
||||
|
||||
`Main-Class: org.springframework.boot.loader.PropertiesLauncher `
|
||||
|
||||
4、**MODULE**,将所有的依赖库打包(`scope`为`provided`的除外),但是不打包 Spring Boot 的任何`Launcher`
|
||||
|
||||
5、**NONE**,将所有的依赖库打包,但是不打包 Spring Boot 的任何 `Launcher`
|
||||
|
||||
无论启动类是 `JarLauncher` 或者 `PropertiesLauncher`,`loader.path` 引入的 Jar 和 Spring Boot jar 包中 `BOOT-INFO/lib` 包下 Jar 都是使用类加载器 `org.springframework.boot.loader.LaunchedURLClassLoader` 进行加载,也就是说他们使用的是同一个类加载器。这个类加载器主要就是负责加载 Jar 包里的 Jar 的加载,因为JDK没有实现该功能所以 SpringBoot 定义了该类
|
||||
|
||||
注意到同一个程序,打包成不同类型时,`PropertiesLauncher (20s)` 比 `JarLauncher (8s)` 启动慢很多。
|
||||
|
||||
##### **方案3:自定义类加载器,加载固定目录下的jar包或者读取环境变量路径**
|
||||
|
||||
这种方式需要程序启动的某个节点,调用自定义类加载器去加载指定目录下的 Jar 包,时间点不是很好控制。但这个定制化程度比较高。
|
||||
|
||||
`//可以在这里调用自定义类加载 SpringApplication.run(SpringBootDemo.class, args); `
|
||||
|
||||
##### **方案4:通过设置-Xbootclasspath/a或者 Extention ClassLoader通过改变参数java.ext.dirs实现**
|
||||
|
||||
会改变系统类加载器加载行为不安全不推荐!!!
|
||||
|
||||
#### **加载后的类如何扫描进入Spring容器**
|
||||
|
||||
对于业务 Jar 包类,可以预先在SpringBoot中指定好扫描包路径,那么只需要插件包路径是`com.demo`开头即可,如下
|
||||
|
||||
`@SpringBootApplication(scanBasePackages = {"com.demo"}) public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); } `
|
||||
|
||||
如不符合包名前缀一致的情况,需要在jar包`META-INF`下的`spring.factories`指定好自动装配的类即可如 mybatis-plus:
|
||||
|
||||
`# Auto Configure org.springframework.boot.env.EnvironmentPostProcessor=\ com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\ com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\ com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration `
|
||||
|
||||
👉 欢迎[加入小哈的星球](https://mp.weixin.qq.com/s?__biz=MzU4MDUyMDQyNQ==&mid=2247538511&idx=1&sn=c2f80c1f6143af7bfcaa661951d7bf23&chksm=fd5787c9ca200edf9b861f51dcdc6e15a575488ab6be0f5b721ea27047cf3316e07ea33a9a8d&token=343952052&lang=zh_CN&scene=21#wechat_redirect),你将获得: **专属的项目实战(多个项目) / 1v1 提问 / **Java 学习路线 /** 学习打卡 / 每月赠书 / 社群讨论**
|
||||
@@ -0,0 +1,4 @@
|
||||
[https://blog.51cto.com/u_15659694/5755246](https://blog.51cto.com/u_15659694/5755246)
|
||||
|
||||
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[https://blog.51cto.com/u_15659694/5755246](https://blog.51cto.com/u_15659694/5755246)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
Arthas是一款功能强大的Java诊断工具,帮你快速定位线上应用问题。它的核心功能包括**系统状态监控、方法调用追踪、性能热点分析**以及**动态代码修改**等。
|
||||
|
||||
为了让你快速上手,我先用一个表格汇总其最常用的命令、核心功能和应用场景:
|
||||
|
||||
| 命令 | 核心功能与参数 | 典型应用场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| **dashboard** | 实时显示JVM内存、线程、GC、系统负载等信息。 | 快速概览应用整体健康状态,初步判断CPU、内存或线程问题。 |
|
||||
| **thread** | 查看所有线程状态。`thread -n 3`查看最忙的3个线程;`thread id`查看指定线程堆栈。 | 定位CPU飙高、线程死锁、线程卡顿。 |
|
||||
| **trace** | 追踪方法内部调用路径,并统计每个节点的耗时。`trace ClassName methodName`。 | 分析接口或方法响应慢,找出内部调用的性能瓶颈。 |
|
||||
| **watch** | 观察方法的入参、返回值或异常。`watch ClassName methodName "{params, returnObj}"`。 | 动态监测方法调用时的数据,用于排查参数异常或返回值错误。 |
|
||||
| **jad** | 反编译已加载的类到源代码。`jad --source-only ClassName`。 | 确认线上运行的代码是否为预期版本,或在无源码时理解逻辑。 |
|
||||
| **sc** | 查看JVM已加载的类信息。`sc -d *ClassName*`显示类详情(含ClassLoader)。 | 排查类找不到、类冲突,或为`jad`、`mc`等命令提供ClassLoader信息。 |
|
||||
| **heapdump** | 生成JVM堆转储文件(`.hprof`)。 | 转储内存快照,供MAT、VisualVM等工具进行离线内存泄漏分析。 |
|
||||
| **profiler** | 生成CPU/内存火焰图,分析性能热点。`profiler start`/`profiler stop`。 | **深度分析CPU占用过高或内存分配频繁的根本原因**。 |
|
||||
|
||||
### 🔥 如何生成与分析火焰图
|
||||
火焰图能直观展示CPU时间或内存分配在方法调用栈上的分布,是分析性能问题的利器。在Arthas中,主要通过 **`profiler`命令**(基于async-profiler)来实现。
|
||||
|
||||
**1. 生成火焰图**
|
||||
```bash
|
||||
# 1. 启动性能采样(默认采样CPU事件)
|
||||
[arthas@12345]$ profiler start
|
||||
# 可指定采样事件,如分析内存分配:profiler start --event alloc
|
||||
|
||||
# 2. 让应用运行一段时间(如执行某个压测或复现问题场景)
|
||||
|
||||
# 3. 停止采样并生成结果(默认生成HTML格式的火焰图)
|
||||
[arthas@12345]$ profiler stop --format html
|
||||
```
|
||||
停止后,命令行会输出火焰图文件的保存路径,通常位于 `arthas-output/` 目录下。
|
||||
|
||||
**2. 查看火焰图**
|
||||
生成后,你可以直接在浏览器中打开该HTML文件。如果Arthas在本地运行,也可以通过其内置的HTTP服务访问:`http://localhost:3658/arthas-output/`。
|
||||
|
||||
**3. 解读火焰图**
|
||||
- **看布局**:火焰图是**倒置的调用栈**。**每一层代表一个方法调用,下层是上层方法的调用者**。
|
||||
- **找热点**:**横向条块的宽度代表该方法的资源(如CPU时间)占用比例,越宽表示消耗越多**。重点关注最顶部的**“平顶山”**(即一个较宽的方法条块出现在多个调用栈顶部),这通常是性能瓶颈所在。
|
||||
- **分析路径**:结合从下至上的调用链,可以定位到具体是哪条业务执行路径导致了高消耗。
|
||||
|
||||
### 💡 一些实用技巧
|
||||
- **退出与重连**:在Arthas会话中,输入 `stop` 会完全退出Arthas;输入 `quit` 或 `exit` 仅退出当前会话,Attach的进程不受影响。
|
||||
- **条件过滤**:`watch` 和 `trace` 等命令支持条件表达式。例如,`watch ... 'params[0]=="keyword"'` 可以只在第一个参数为特定值时触发观察。
|
||||
- **批处理执行**:可以将多个命令写入脚本文件,通过 `as.sh -f your.as` 一次性执行,适合自动化检查。
|
||||
|
||||
### 🛠️ 典型问题排查流程参考
|
||||
当遇到线上问题时,可以遵循以下步骤使用Arthas:
|
||||
1. **初步定位**:使用 `dashboard` 或 `thread -n 3`,快速判断是CPU、内存还是线程问题。
|
||||
2. **链路追踪**:如果某个接口慢,使用 `trace` 追踪该方法,找到耗时最长的子调用。
|
||||
3. **数据观察**:对疑似有问题的方法使用 `watch`,观察其入参和返回值是否异常。
|
||||
4. **深度分析**:如果发现是某个方法本身消耗大量CPU,使用 `profiler` 生成火焰图,从微观层面分析其内部调用热点。
|
||||
5. **代码确认**:如有必要,使用 `jad` 反编译线上运行的类,确认代码逻辑。
|
||||
|
||||
如果你能告诉我你具体遇到了哪一类性能问题(例如CPU持续过高、接口延迟、内存缓慢增长),我可以为你提供更有针对性的排查思路和命令组合。
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
cp参数就是classpath
|
||||
|
||||
```shell
|
||||
java -cp ".;dir/;dir/a.jar;dir/b.jar" Main
|
||||
```
|
||||
|
||||
从java1.6开始,cp参数支持通配符匹配jar文件。需要注意两点:
|
||||
|
||||
1. 用引号`"`
|
||||
2. 用`*`,而不是`*.jar`
|
||||
|
||||
##### Windows:
|
||||
|
||||
```shell
|
||||
java -cp "Test.jar;lib/*" my.package.MainClass
|
||||
```
|
||||
|
||||
##### Linux:
|
||||
|
||||
```shell
|
||||
java -cp "Test.jar:lib/*" my.package.MainClass
|
||||
```
|
||||
|
||||
若不支持通配符,bash shell支持用以下方式:
|
||||
|
||||
```shell
|
||||
java -cp $(echo lib/*.jar | tr ' ' ':')
|
||||
```
|
||||
|
||||
其中先执行`$()`括号中的命令`echo lib/*.jar | tr ' ' ':'`,然后把`$(echo lib/*.jar | tr ' ' ':')`部分替换为命令返回的字符串,再执行替换后的`java`命令。
|
||||
`tr ' ' ':'`会把`空格`替换为`:`
|
||||
|
||||
##### 注意:
|
||||
|
||||
- `-cp "foo/*"`只表示foo目录下的jar,不包括foo下的class,若要包括则用`-cp "foo;foo/*"`或`-cp "foo/*;foo"`,这里的顺序会决定是先加载jar还是class。
|
||||
- `foo/*`不会递归加载foo下子目录的jar。
|
||||
- `foo/*`加载foo目录下的jar顺序不是固定的,所以程序不应该依赖固定的顺序。若需要依赖则用枚举方式而不是通配符方式添加。
|
||||
- manifest.mf中的Class-Path不支持通配符。
|
||||
@@ -0,0 +1,13 @@
|
||||
如果你发送的数据来源于文件(且不需要在应用层修改),可以使用 `transferTo` 直接利用操作系统的 `sendfile` 系统调用,完全避免数据拷贝到用户空间:
|
||||
|
||||
|
||||
FileChannel fileChannel = FileChannel.open(Paths.get("/path/to/file"));
|
||||
SocketChannel socketChannel = SocketChannel.open();
|
||||
socketChannel.connect(address);
|
||||
// 将文件从 offset 开始,共 count 字节直接发往 socket
|
||||
fileChannel.transferTo(offset, count, socketChannel);
|
||||
|
||||
这种情况下,数据直接从文件系统的 page cache 进入 socket 缓冲区,**完全不经过用户空间**,零拷贝。
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
[Lombok](https://so.csdn.net/so/search?q=Lombok&spm=1001.2101.3001.7020)是一个Java库,它通过注解的方式来简化Java代码的编写。
|
||||
|
||||
## 1、Lombok库中常用的注解
|
||||
|
||||
@Getter:自动生成gette[r方](https://so.csdn.net/so/search?q=r%E6%96%B9&spm=1001.2101.3001.7020)法。
|
||||
|
||||
@Setter:自动生成setter方法。
|
||||
|
||||
@ToString:自动生成[toString方法](https://so.csdn.net/so/search?q=toString%E6%96%B9%E6%B3%95&spm=1001.2101.3001.7020)。
|
||||
|
||||
@EqualsAndHashCode:自动生成equals和hashCode方法。
|
||||
|
||||
@NoArgsConstructor:自动生成无参构造方法。
|
||||
|
||||
@AllArgsConstructor:自动生成全参构造方法。
|
||||
|
||||
@Data:自动生成getter、setter、equals、hashCode和toString方法。
|
||||
|
||||
@Builder:自动生成Builder模式的代码。
|
||||
|
||||
```TypeScript
|
||||
User user = User.builder()
|
||||
.name("Tom")
|
||||
.age(20)
|
||||
.isVip(true)
|
||||
.build();
|
||||
|
||||
12345
|
||||
```
|
||||
|
||||
@Slf4j:自动生成log变量,可以使用log.info()等方法输出日志。
|
||||
|
||||
@NonNull:标注在参数上,生成非空检查代码。
|
||||
|
||||
@Cleanup:自动生成try-finally代码块,用于自动关闭流等资源。
|
||||
|
||||
@Synchronized:自动生成同步代码块。
|
||||
以上是Lombok库中常用的注解,使用这些注解可以大大简化Java代码的编写,减少了代码的冗余。
|
||||
|
||||
@Accessors
|
||||
|
||||
@Accessors(chain=true)
|
||||
链式访问,该注解设置chain=true,生成setter方法返回this(也就是返回的是对象),代替了默认的返回void。
|
||||
|
||||
```TypeScript
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class User {
|
||||
private Integer id;
|
||||
private String name;
|
||||
private Integer age;
|
||||
|
||||
public static void main(String[] args) {
|
||||
//开起chain=true后可以使用链式的set
|
||||
User user=new User().setAge(31).setName("pollyduan");//返回对象
|
||||
System.out.println(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1234567891011121314
|
||||
```
|
||||
|
||||
@Accessors(fluent = true)
|
||||
与chain=true类似,区别在于getter和setter不带set和get前缀。
|
||||
|
||||
```TypeScript
|
||||
@Data
|
||||
@Accessors(fluent=true)
|
||||
public class User {
|
||||
private Integer id;
|
||||
private String name;
|
||||
private Integer age;
|
||||
|
||||
public static void main(String[] args) {
|
||||
//fluent=true开启后默认chain=true,故这里也可以使用链式set
|
||||
User user=new User().age(31).name("pollyduan");//不需要写set
|
||||
System.out.println(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1234567891011121314
|
||||
```
|
||||
|
||||
@Accessors(prefix = “f”)
|
||||
set方法忽略指定的前缀。不推荐大神们这样去命名。
|
||||
|
||||
```TypeScript
|
||||
@Data
|
||||
@Accessors(prefix = "f")
|
||||
public class User {
|
||||
private String fName = "Hello, World!";
|
||||
|
||||
public static void main(String[] args) {
|
||||
User user=new User();
|
||||
user.setName("pollyduan");//注意方法名
|
||||
System.out.println(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
123456789101112
|
||||
```
|
||||
|
||||
## 2、@ToString注解可能引起的问题
|
||||
|
||||
ToString可能会引起堆栈溢出,
|
||||
1、当存在如下两个相互依赖的类时,并且使用了@ToString注解
|
||||
|
||||
```TypeScript
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class User implements Serializable {
|
||||
private Person person;
|
||||
}
|
||||
|
||||
123456
|
||||
```
|
||||
|
||||
```TypeScript
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class Person implements Serializable {
|
||||
private User user;
|
||||
}
|
||||
|
||||
123456
|
||||
```
|
||||
|
||||
2、两个对象相互依赖的情况下,并且调用了toString()方法。会无限递归调用tostring方法,且创建字符串造成内存溢出。
|
||||
|
||||
```TypeScript
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Person person = new Person();
|
||||
User user = new User();
|
||||
person.setUser(user); // 只要任意一个引用为空,就不会内存溢出(例如:person.setUser(null))
|
||||
user.setPerson(person);
|
||||
System.out.println(user.toString()); // 要调用tostring方法才会无限递归
|
||||
}
|
||||
|
||||
1234567
|
||||
```
|
||||
|
||||
因此在使用@ToString和@Data的时候要注意这种情况。
|
||||
可以使用@ToString.Exclude注解来防止无限递归
|
||||
|
||||
```TypeScript
|
||||
@ToString.Exclude
|
||||
private Person person;
|
||||
|
||||
12
|
||||
```
|
||||
|
||||
## 3、@Data注解可能引起的问题
|
||||
|
||||
Lombok中的@Data注解是一个方便的注解,可以自动生成getter、setter、equals、hashCode和toString方法,简化了Java代码的编写。但是,如果使用不当,可能会出现以下问题:
|
||||
|
||||
- toString()方法可能引发无限递归:当一个类中的属性是另一个类的对象时,如果两个类都使用了@Data注解,就会出现toString()方法的无限递归,导致栈溢出。可以通过使用@ToString.Exclude注解来排除不需要在toString()方法中输出的属性。(上面已讲)
|
||||
|
||||
- 可能会降低代码的可读性:虽然@Data注解可以大大简化Java代码的编写,但是有时候自动生成的代码可能会比较复杂,降低了代码的可读性。可以根据实际情况选择使用@Data注解或手动编写getter、setter、equals、hashCode和toString方法。
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
[Lombok](https://so.csdn.net/so/search?q=Lombok&spm=1001.2101.3001.7020)是一个Java库,它通过注解的方式来简化Java代码的编写。
|
||||
|
||||
## 1、Lombok库中常用的注解
|
||||
|
||||
@Getter:自动生成gette[r方](https://so.csdn.net/so/search?q=r%E6%96%B9&spm=1001.2101.3001.7020)法。
|
||||
|
||||
@Setter:自动生成setter方法。
|
||||
|
||||
@ToString:自动生成[toString方法](https://so.csdn.net/so/search?q=toString%E6%96%B9%E6%B3%95&spm=1001.2101.3001.7020)。
|
||||
|
||||
@EqualsAndHashCode:自动生成equals和hashCode方法。
|
||||
|
||||
@NoArgsConstructor:自动生成无参构造方法。
|
||||
|
||||
@AllArgsConstructor:自动生成全参构造方法。
|
||||
|
||||
@Data:自动生成getter、setter、equals、hashCode和toString方法。
|
||||
|
||||
@Builder:自动生成Builder模式的代码。
|
||||
|
||||
```TypeScript
|
||||
User user = User.builder()
|
||||
.name("Tom")
|
||||
.age(20)
|
||||
.isVip(true)
|
||||
.build();
|
||||
|
||||
12345
|
||||
```
|
||||
|
||||
@Slf4j:自动生成log变量,可以使用log.info()等方法输出日志。
|
||||
|
||||
@NonNull:标注在参数上,生成非空检查代码。
|
||||
|
||||
@Cleanup:自动生成try-finally代码块,用于自动关闭流等资源。
|
||||
|
||||
@Synchronized:自动生成同步代码块。
|
||||
以上是Lombok库中常用的注解,使用这些注解可以大大简化Java代码的编写,减少了代码的冗余。
|
||||
|
||||
@Accessors
|
||||
|
||||
@Accessors(chain=true)
|
||||
链式访问,该注解设置chain=true,生成setter方法返回this(也就是返回的是对象),代替了默认的返回void。
|
||||
|
||||
```TypeScript
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class User {
|
||||
private Integer id;
|
||||
private String name;
|
||||
private Integer age;
|
||||
|
||||
public static void main(String[] args) {
|
||||
//开起chain=true后可以使用链式的set
|
||||
User user=new User().setAge(31).setName("pollyduan");//返回对象
|
||||
System.out.println(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1234567891011121314
|
||||
```
|
||||
|
||||
@Accessors(fluent = true)
|
||||
与chain=true类似,区别在于getter和setter不带set和get前缀。
|
||||
|
||||
```TypeScript
|
||||
@Data
|
||||
@Accessors(fluent=true)
|
||||
public class User {
|
||||
private Integer id;
|
||||
private String name;
|
||||
private Integer age;
|
||||
|
||||
public static void main(String[] args) {
|
||||
//fluent=true开启后默认chain=true,故这里也可以使用链式set
|
||||
User user=new User().age(31).name("pollyduan");//不需要写set
|
||||
System.out.println(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1234567891011121314
|
||||
```
|
||||
|
||||
@Accessors(prefix = “f”)
|
||||
set方法忽略指定的前缀。不推荐大神们这样去命名。
|
||||
|
||||
```TypeScript
|
||||
@Data
|
||||
@Accessors(prefix = "f")
|
||||
public class User {
|
||||
private String fName = "Hello, World!";
|
||||
|
||||
public static void main(String[] args) {
|
||||
User user=new User();
|
||||
user.setName("pollyduan");//注意方法名
|
||||
System.out.println(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
123456789101112
|
||||
```
|
||||
|
||||
## 2、@ToString注解可能引起的问题
|
||||
|
||||
ToString可能会引起堆栈溢出,
|
||||
1、当存在如下两个相互依赖的类时,并且使用了@ToString注解
|
||||
|
||||
```TypeScript
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class User implements Serializable {
|
||||
private Person person;
|
||||
}
|
||||
|
||||
123456
|
||||
```
|
||||
|
||||
```TypeScript
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class Person implements Serializable {
|
||||
private User user;
|
||||
}
|
||||
|
||||
123456
|
||||
```
|
||||
|
||||
2、两个对象相互依赖的情况下,并且调用了toString()方法。会无限递归调用tostring方法,且创建字符串造成内存溢出。
|
||||
|
||||
```TypeScript
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Person person = new Person();
|
||||
User user = new User();
|
||||
person.setUser(user); // 只要任意一个引用为空,就不会内存溢出(例如:person.setUser(null))
|
||||
user.setPerson(person);
|
||||
System.out.println(user.toString()); // 要调用tostring方法才会无限递归
|
||||
}
|
||||
|
||||
1234567
|
||||
```
|
||||
|
||||
因此在使用@ToString和@Data的时候要注意这种情况。
|
||||
可以使用@ToString.Exclude注解来防止无限递归
|
||||
|
||||
```TypeScript
|
||||
@ToString.Exclude
|
||||
private Person person;
|
||||
|
||||
12
|
||||
```
|
||||
|
||||
## 3、@Data注解可能引起的问题
|
||||
|
||||
Lombok中的@Data注解是一个方便的注解,可以自动生成getter、setter、equals、hashCode和toString方法,简化了Java代码的编写。但是,如果使用不当,可能会出现以下问题:
|
||||
|
||||
- toString()方法可能引发无限递归:当一个类中的属性是另一个类的对象时,如果两个类都使用了@Data注解,就会出现toString()方法的无限递归,导致栈溢出。可以通过使用@ToString.Exclude注解来排除不需要在toString()方法中输出的属性。(上面已讲)
|
||||
|
||||
- 可能会降低代码的可读性:虽然@Data注解可以大大简化Java代码的编写,但是有时候自动生成的代码可能会比较复杂,降低了代码的可读性。可以根据实际情况选择使用@Data注解或手动编写getter、setter、equals、hashCode和toString方法。
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# 依赖范围与 classpath 的关系表
|
||||
|
||||

|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,4 @@
|
||||
# 依赖范围与 classpath 的关系表
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
|
||||
```Plain Text
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>2.1.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<version>1.1.9</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
springboot 2.3.4.Release版本配置文件配置
|
||||
|
||||
```Plain Text
|
||||
spring:
|
||||
datasource:
|
||||
username: root
|
||||
password: root
|
||||
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
#项目启动执行建表语句,classpath指定sql文件的位置
|
||||
schema:
|
||||
- classpath:sql/init.sql
|
||||
initialization-mode: always
|
||||
platform: mysql
|
||||
sql-script-encoding: UTF-8
|
||||
```
|
||||
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
springboot 2.6.4版本配置文件配置(谢谢nreg的指出)
|
||||
|
||||
spring:
|
||||
sql:
|
||||
init:
|
||||
separator: ; #断句分隔符,默认为 ; 号,如果执行语句中有 存储过程 或 游标 这种 则需要更改
|
||||
encoding: UTF-8
|
||||
platform: sqlite #默认值为all 在不同数据库切换的情况下才使用 all 配置,spring boot 会自动检测当前使用的数据库
|
||||
mode: always # 初始化模式,有三个值:always:始终执行初始化、embedded:只初始化内存数据库(默认)、never:不执行初始化
|
||||
schema-locations: #表初始化语句 DDL,默认加载 schema.sql
|
||||
- classpath:sql/schema.sql
|
||||
data-locations: #数据初始化 DQL(数据查询)或 DML(数据操作),默认加载 data.sql,
|
||||
- classpath:sql/data.sql
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
sql文件填写
|
||||
|
||||
---
|
||||
|
||||
-- Table structure for t1
|
||||
|
||||
---
|
||||
|
||||
DROP TABLE IF EXISTS `t1`;
|
||||
CREATE TABLE `t1` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
---
|
||||
|
||||
-- Records of t1
|
||||
|
||||
---
|
||||
|
||||
INSERT INTO `t1` VALUES (1, '1');
|
||||
INSERT INTO `t1` VALUES (2, '2');
|
||||
INSERT INTO `t1` VALUES (3, '3');
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
————————————————
|
||||
版权声明:本文为CSDN博主「知识浅谈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
|
||||
原文链接:[https://blog.csdn.net/qq_37699336/article/details/119877899](https://blog.csdn.net/qq_37699336/article/details/119877899)
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
|
||||
|
||||
```Plain Text
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>2.1.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<version>1.1.9</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
springboot 2.3.4.Release版本配置文件配置
|
||||
|
||||
```Plain Text
|
||||
spring:
|
||||
datasource:
|
||||
username: root
|
||||
password: root
|
||||
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
#项目启动执行建表语句,classpath指定sql文件的位置
|
||||
schema:
|
||||
- classpath:sql/init.sql
|
||||
initialization-mode: always
|
||||
platform: mysql
|
||||
sql-script-encoding: UTF-8
|
||||
```
|
||||
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
springboot 2.6.4版本配置文件配置(谢谢nreg的指出)
|
||||
|
||||
spring:
|
||||
sql:
|
||||
init:
|
||||
separator: ; #断句分隔符,默认为 ; 号,如果执行语句中有 存储过程 或 游标 这种 则需要更改
|
||||
encoding: UTF-8
|
||||
platform: sqlite #默认值为all 在不同数据库切换的情况下才使用 all 配置,spring boot 会自动检测当前使用的数据库
|
||||
mode: always # 初始化模式,有三个值:always:始终执行初始化、embedded:只初始化内存数据库(默认)、never:不执行初始化
|
||||
schema-locations: #表初始化语句 DDL,默认加载 schema.sql
|
||||
- classpath:sql/schema.sql
|
||||
data-locations: #数据初始化 DQL(数据查询)或 DML(数据操作),默认加载 data.sql,
|
||||
- classpath:sql/data.sql
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
sql文件填写
|
||||
|
||||
---
|
||||
|
||||
-- Table structure for t1
|
||||
|
||||
---
|
||||
|
||||
DROP TABLE IF EXISTS `t1`;
|
||||
CREATE TABLE `t1` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
---
|
||||
|
||||
-- Records of t1
|
||||
|
||||
---
|
||||
|
||||
INSERT INTO `t1` VALUES (1, '1');
|
||||
INSERT INTO `t1` VALUES (2, '2');
|
||||
INSERT INTO `t1` VALUES (3, '3');
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
————————————————
|
||||
版权声明:本文为CSDN博主「知识浅谈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
|
||||
原文链接:[https://blog.csdn.net/qq_37699336/article/details/119877899](https://blog.csdn.net/qq_37699336/article/details/119877899)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
DK 1.5 后推出的 ConcurrentHashMap,是一个高性能的线程安全的哈希表容器。“线程安全”这四个字特别容易让人误解,因为 ConcurrentHashMap 能保证提供的原子性读写操作是线程安全的。
|
||||
|
||||
我们可以举一个形象的例子。ConcurrentHashMap 就像是一个大篮子,现在这个篮子里有 900 个桔子,我们期望把这个篮子装满 1000 个桔子,也就是再装 100 个桔子。有 10 个工人来干这件事儿,大家先后到岗后会计算还需要补多少个桔子进去,最后把桔子装入篮子。
|
||||
|
||||
ConcurrentHashMap 这个篮子本身,可以确保多个工人在装东西进去时,不会相互影响干扰,但无法确保工人 A 看到还需要装 100 个桔子但是还未装的时候,工人 B 就看不到篮子中的桔子数量。更值得注意的是,你往这个篮子装 100 个桔子的操作不是原子性的,在别人看来可能会有一个瞬间篮子里有 964 个桔子,还需要补 36 个桔子。
|
||||
|
||||
回到 ConcurrentHashMap,我们需要注意 ConcurrentHashMap 对外提供的方法或能力的限制:
|
||||
|
||||
1、使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
|
||||
|
||||
2、诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
|
||||
|
||||
3、诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。
|
||||
@@ -0,0 +1,125 @@
|
||||
FastThreadLocal 从字面意义上来看,它是“Fast”+“ThreadLocal”的结合体,寓意为快速的 ThreadLocal。那么,问题来了,Netty 为什么要再造一个 FastThreadLocal?FastThreadLocal 运行快的原因是啥?除了快之外,它还有其他优势吗?
|
||||
|
||||
### 1.先从ThreadLocal说起
|
||||
|
||||
ThreadLocal 线程本地变量,每个线程都拥有一份该变量的独立副本,即使是在多线程环境下,每个线程也只能修改和访问自己的那份副本,从而避免了线程安全问题,实现了线程间的隔离。
|
||||
|
||||
ThreadLocal 底层是使用 ThreadLocalMap 实现的,这点从 JDK 的源码中可以看出,核心源码如下:
|
||||
|
||||
```java
|
||||
private void set(Thread t, T value) {
|
||||
ThreadLocalMap map = getMap(t);
|
||||
if (map != null) {
|
||||
map.set(this, value);
|
||||
} else {
|
||||
createMap(t, value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从 ThreadLocal 的 set 方法可以看出,ThreadLocal 是存储在 ThreadLocalMap 中的,咱们继续看 ThreadLocalMap 的源码实现:
|
||||
|
||||

|
||||
|
||||
从上面源码可以看出,ThreadLocalMap 中存放的是 Entry(哈希桶),而 Entry 中的 key 就是 ThreadLocal,而 value 则是要存储的值,所以我们得出 ThreadLocal 的底层实现如下:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 2.ThreadLocal存在的问题
|
||||
|
||||
### 2.1 性能问题
|
||||
|
||||
因为 ThreadLocal 底层是使用 ThreadLocalMap 实现的,ThreadLocalMap 类似于哈希表。当一个线程是拥有多个 ThreadLocal 时,ThreadLocalMap 很容易发生 Hash 冲突,此时 ThreadLocal 就不得不使用线性探测法来解决哈希冲突了,而在解决 Hash 冲突时需要不停地向下寻找,效率较低,因此 ThreadLocal 存在的第一个问题就是性能较低。
|
||||
|
||||
### 2.2 内存泄漏问题
|
||||
|
||||
ThreadLocal 也存在内存泄漏的问题,具体来说 ThreadLocalMap 使用 ThreadLocal 对象作为键(Key),并且这个键是弱引用(WeakReference)类型。这意味着当没有其他强引用指向 ThreadLocal 对象时,它将会在下次垃圾回收时被回收。然而,Entry 中保存的值(Value)仍然是强引用,这就可能导致以下问题:
|
||||
|
||||
1. **弱引用键的回收**:一旦外部对 ThreadLocal 实例的所有强引用消失,ThreadLocal 对象本身就会变为弱可达状态。在下一次垃圾回收时,由于是弱引用,ThreadLocal 对象会被回收,但 Entry 中的 Value(即实际存储的数据)仍然是强引用,因此不会被回收。
|
||||
2. **Map 引用陷阱**:即使 ThreadLocal 键被回收,Entry 仍然存在于 ThreadLocalMap 中,并且由于 Map 对 Entry 的引用,这些 Entry 所持有的 Value 对象也不会被垃圾回收,从而导致这些对象无法被使用也无法被释放,形成了所谓的“内存泄漏”。
|
||||
3. **线程长期存活**:在一些场景下,特别是使用线程池时,线程的生命周期往往很长,甚至伴随整个应用的生命周期。这意味着 ThreadLocalMap 中的 Entry 可能会长时间不被清理,进一步加剧了内存泄漏问题。
|
||||
|
||||
所以,综合来看,在使用 ThreadLocal 时,如果在使用完之后,未及时调用 remove() 方法的话,就会出现内存泄漏的问题。
|
||||
|
||||
### 3.FastThreadLocal特点
|
||||
|
||||
为了解决 ThreadLocal 存在的这些问题,所以 Netty 创造出了一个 FastThreadLocal,FastThreadLocal 的特点如下。
|
||||
|
||||
### 3.1 效率高
|
||||
|
||||
FastThreadLocal 之所以性能高的原因是因为其存储结构,在 FastThreadLocal 中并没有向 ThreadLocal 那样,使用哈希表来存储元素,而是使用了数组来进行元素存储,它的核心实现源码如下:
|
||||
|
||||
```java
|
||||
public class FastThreadLocal<V> {
|
||||
// FastThreadLocal中的index是记录了该它维护的数据应该存储的位置
|
||||
// InternalThreadLocalMap数组中的下标, 它是在构造函数中确定的
|
||||
private final int index;
|
||||
|
||||
public InternalThreadLocal() {
|
||||
index = InternalThreadLocalMap.nextVariableIndex();
|
||||
}
|
||||
// 省略其他代码
|
||||
}
|
||||
```
|
||||
|
||||
FastThreadLocal 核心类 InternalThreadLocalMap 的实现源码如下:
|
||||
|
||||
```java
|
||||
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
|
||||
// 自增索引, ⽤于计算下次存储到Object数组中的位置
|
||||
private static final AtomicInteger nextIndex = new AtomicInteger();
|
||||
|
||||
private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
|
||||
|
||||
public static int nextVariableIndex() {
|
||||
int index = nextIndex.getAndIncrement();
|
||||
if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {
|
||||
nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);
|
||||
throw new IllegalStateException("too many thread-local indexed variables");
|
||||
}
|
||||
return index;
|
||||
}
|
||||
// 省略其他代码
|
||||
}
|
||||
```
|
||||
|
||||
从上述源码可以看出,FastThreadLocal 在初始化的时候分配一个数组索引 index,index 的值采用原子类 AtomicInteger 保证顺序递增,通过调用 InternalThreadLocalMap.nextVariableIndex() 方法获得。然后在读写数据的时候通过数组下标 index 直接定位到 FastThreadLocal 的位置,时间复杂度为 O(1)。如果数组下标递增到非常大,那么数组也会比较大,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能。
|
||||
|
||||
因此,在 FastThreadLocal 中并不需要使用线性探测法来解决 Hash 冲突,因为它是使用数组进行存储的,每次使用下标进行查询即可,它的查询时间复杂度也是 O(1) 的,所以它的操作效率很高。
|
||||
|
||||
### 3.2 安全性更高
|
||||
|
||||
JDK 原生的 ThreadLocal 使用不当可能造成内存泄漏,只能等待线程销毁。**然而 FastThreadLocal 却不存在这个问题,在 FastThreadLocal 中不仅提供了 remove() 方法可以主动清除对象,而且它还封装了 FastThreadLocalRunnable,FastThreadLocalRunnable 在最后使用完之后会自动调用 removeAll() 方法将集合中所有对象清理掉,因此 FastThreadLocal 更安全。**
|
||||
|
||||
FastThreadLocalRunnable 自动清除对象的实现核心源码如下:
|
||||
|
||||
```java
|
||||
final class FastThreadLocalRunnable implements Runnable {
|
||||
private final Runnable runnable;
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
FastThreadLocal.removeAll();
|
||||
}
|
||||
}
|
||||
static Runnable wrap(Runnable runnable) {
|
||||
return runnable instanceof FastThreadLocalRunnable
|
||||
? runnable : new FastThreadLocalRunnable(runnable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.小结
|
||||
|
||||
FastThreadLocal 相比于 ThreadLocal 存在以下两个主要优点:
|
||||
|
||||
1. **性能更高**:FastThreadLocal 使用了数组的方式来存储元素,所以它的查询时间复杂度 O(1) 相比于 ThreadLocal 的哈希表操作效率更高。
|
||||
2. **安全性更高**:FastThreadLocal 中的 FastThreadLocalRunnable 在最后执行完之后会自动调用 removeAll() 将集合中所有对象都清理掉,可以避免内存泄漏的问题,所以它的安全性更高。
|
||||
|
||||
### 课后思考
|
||||
|
||||
FastThreadLocal 有什么缺点?使用 FastThreadLocal 一定比 ThreadLocal 性能高吗?
|
||||
@@ -0,0 +1,125 @@
|
||||
FastThreadLocal 从字面意义上来看,它是“Fast”+“ThreadLocal”的结合体,寓意为快速的 ThreadLocal。那么,问题来了,Netty 为什么要再造一个 FastThreadLocal?FastThreadLocal 运行快的原因是啥?除了快之外,它还有其他优势吗?
|
||||
|
||||
### 1.先从ThreadLocal说起
|
||||
|
||||
ThreadLocal 线程本地变量,每个线程都拥有一份该变量的独立副本,即使是在多线程环境下,每个线程也只能修改和访问自己的那份副本,从而避免了线程安全问题,实现了线程间的隔离。
|
||||
|
||||
ThreadLocal 底层是使用 ThreadLocalMap 实现的,这点从 JDK 的源码中可以看出,核心源码如下:
|
||||
|
||||
```java
|
||||
private void set(Thread t, T value) {
|
||||
ThreadLocalMap map = getMap(t);
|
||||
if (map != null) {
|
||||
map.set(this, value);
|
||||
} else {
|
||||
createMap(t, value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从 ThreadLocal 的 set 方法可以看出,ThreadLocal 是存储在 ThreadLocalMap 中的,咱们继续看 ThreadLocalMap 的源码实现:
|
||||
|
||||

|
||||
|
||||
从上面源码可以看出,ThreadLocalMap 中存放的是 Entry(哈希桶),而 Entry 中的 key 就是 ThreadLocal,而 value 则是要存储的值,所以我们得出 ThreadLocal 的底层实现如下:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 2.ThreadLocal存在的问题
|
||||
|
||||
### 2.1 性能问题
|
||||
|
||||
因为 ThreadLocal 底层是使用 ThreadLocalMap 实现的,ThreadLocalMap 类似于哈希表。当一个线程是拥有多个 ThreadLocal 时,ThreadLocalMap 很容易发生 Hash 冲突,此时 ThreadLocal 就不得不使用线性探测法来解决哈希冲突了,而在解决 Hash 冲突时需要不停地向下寻找,效率较低,因此 ThreadLocal 存在的第一个问题就是性能较低。
|
||||
|
||||
### 2.2 内存泄漏问题
|
||||
|
||||
ThreadLocal 也存在内存泄漏的问题,具体来说 ThreadLocalMap 使用 ThreadLocal 对象作为键(Key),并且这个键是弱引用(WeakReference)类型。这意味着当没有其他强引用指向 ThreadLocal 对象时,它将会在下次垃圾回收时被回收。然而,Entry 中保存的值(Value)仍然是强引用,这就可能导致以下问题:
|
||||
|
||||
1. **弱引用键的回收**:一旦外部对 ThreadLocal 实例的所有强引用消失,ThreadLocal 对象本身就会变为弱可达状态。在下一次垃圾回收时,由于是弱引用,ThreadLocal 对象会被回收,但 Entry 中的 Value(即实际存储的数据)仍然是强引用,因此不会被回收。
|
||||
2. **Map 引用陷阱**:即使 ThreadLocal 键被回收,Entry 仍然存在于 ThreadLocalMap 中,并且由于 Map 对 Entry 的引用,这些 Entry 所持有的 Value 对象也不会被垃圾回收,从而导致这些对象无法被使用也无法被释放,形成了所谓的“内存泄漏”。
|
||||
3. **线程长期存活**:在一些场景下,特别是使用线程池时,线程的生命周期往往很长,甚至伴随整个应用的生命周期。这意味着 ThreadLocalMap 中的 Entry 可能会长时间不被清理,进一步加剧了内存泄漏问题。
|
||||
|
||||
所以,综合来看,在使用 ThreadLocal 时,如果在使用完之后,未及时调用 remove() 方法的话,就会出现内存泄漏的问题。
|
||||
|
||||
### 3.FastThreadLocal特点
|
||||
|
||||
为了解决 ThreadLocal 存在的这些问题,所以 Netty 创造出了一个 FastThreadLocal,FastThreadLocal 的特点如下。
|
||||
|
||||
### 3.1 效率高
|
||||
|
||||
FastThreadLocal 之所以性能高的原因是因为其存储结构,在 FastThreadLocal 中并没有向 ThreadLocal 那样,使用哈希表来存储元素,而是使用了数组来进行元素存储,它的核心实现源码如下:
|
||||
|
||||
```java
|
||||
public class FastThreadLocal<V> {
|
||||
// FastThreadLocal中的index是记录了该它维护的数据应该存储的位置
|
||||
// InternalThreadLocalMap数组中的下标, 它是在构造函数中确定的
|
||||
private final int index;
|
||||
|
||||
public InternalThreadLocal() {
|
||||
index = InternalThreadLocalMap.nextVariableIndex();
|
||||
}
|
||||
// 省略其他代码
|
||||
}
|
||||
```
|
||||
|
||||
FastThreadLocal 核心类 InternalThreadLocalMap 的实现源码如下:
|
||||
|
||||
```java
|
||||
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
|
||||
// 自增索引, ⽤于计算下次存储到Object数组中的位置
|
||||
private static final AtomicInteger nextIndex = new AtomicInteger();
|
||||
|
||||
private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
|
||||
|
||||
public static int nextVariableIndex() {
|
||||
int index = nextIndex.getAndIncrement();
|
||||
if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {
|
||||
nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);
|
||||
throw new IllegalStateException("too many thread-local indexed variables");
|
||||
}
|
||||
return index;
|
||||
}
|
||||
// 省略其他代码
|
||||
}
|
||||
```
|
||||
|
||||
从上述源码可以看出,FastThreadLocal 在初始化的时候分配一个数组索引 index,index 的值采用原子类 AtomicInteger 保证顺序递增,通过调用 InternalThreadLocalMap.nextVariableIndex() 方法获得。然后在读写数据的时候通过数组下标 index 直接定位到 FastThreadLocal 的位置,时间复杂度为 O(1)。如果数组下标递增到非常大,那么数组也会比较大,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能。
|
||||
|
||||
因此,在 FastThreadLocal 中并不需要使用线性探测法来解决 Hash 冲突,因为它是使用数组进行存储的,每次使用下标进行查询即可,它的查询时间复杂度也是 O(1) 的,所以它的操作效率很高。
|
||||
|
||||
### 3.2 安全性更高
|
||||
|
||||
JDK 原生的 ThreadLocal 使用不当可能造成内存泄漏,只能等待线程销毁。**然而 FastThreadLocal 却不存在这个问题,在 FastThreadLocal 中不仅提供了 remove() 方法可以主动清除对象,而且它还封装了 FastThreadLocalRunnable,FastThreadLocalRunnable 在最后使用完之后会自动调用 removeAll() 方法将集合中所有对象清理掉,因此 FastThreadLocal 更安全。**
|
||||
|
||||
FastThreadLocalRunnable 自动清除对象的实现核心源码如下:
|
||||
|
||||
```java
|
||||
final class FastThreadLocalRunnable implements Runnable {
|
||||
private final Runnable runnable;
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
FastThreadLocal.removeAll();
|
||||
}
|
||||
}
|
||||
static Runnable wrap(Runnable runnable) {
|
||||
return runnable instanceof FastThreadLocalRunnable
|
||||
? runnable : new FastThreadLocalRunnable(runnable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.小结
|
||||
|
||||
FastThreadLocal 相比于 ThreadLocal 存在以下两个主要优点:
|
||||
|
||||
1. **性能更高**:FastThreadLocal 使用了数组的方式来存储元素,所以它的查询时间复杂度 O(1) 相比于 ThreadLocal 的哈希表操作效率更高。
|
||||
2. **安全性更高**:FastThreadLocal 中的 FastThreadLocalRunnable 在最后执行完之后会自动调用 removeAll() 将集合中所有对象都清理掉,可以避免内存泄漏的问题,所以它的安全性更高。
|
||||
|
||||
### 课后思考
|
||||
|
||||
FastThreadLocal 有什么缺点?使用 FastThreadLocal 一定比 ThreadLocal 性能高吗?
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
|
||||
原创 阿伟的水杯 阿伟的水杯
|
||||
|
||||
_2026年4月15日 17:01_ _广东_
|
||||
|
||||
现在有一个打包好的jar包,如果我想修改jar包里某个类的某段代码,但是我又不会写字节码,那要如何操作呢?
|
||||
|
||||
很简单,主要是利用了javassist库去直接操作打包好的字节码
|
||||
|
||||
先引入:
|
||||
|
||||
```
|
||||
<dependency>
|
||||
```
|
||||
|
||||
然后直接上代码:
|
||||
|
||||
```
|
||||
public static void main(String[] args) throws Exception {
|
||||
```
|
||||
|
||||
运行,就会把那个class单独打包出来了,把class覆盖回jar包就可以了。
|
||||
|
||||
使用命令
|
||||
|
||||
```
|
||||
jar uvf xxxx.jar com/xxx/af/f/e.class
|
||||
```
|
||||
|
||||
就可以直接替换jar包里的类了。
|
||||
|
||||
解析:
|
||||
|
||||
首先是引入包,以及类,这个很简单,使用压缩软件打开jar包就知道对应的类名是什么了:
|
||||
|
||||
```
|
||||
ClassPoolclassPool= ClassPool.getDefault();
|
||||
```
|
||||
|
||||
接着重点就是构造内容:
|
||||
|
||||
```
|
||||
//要改动的方法
|
||||
```
|
||||
|
||||
有多种方法获取对应方法:
|
||||
|
||||

|
||||
|
||||
根据方法的参数情况选择对应的处理方法即可。
|
||||
|
||||
接着是构造方法体的代码:
|
||||
|
||||
javassist不需要写字节码,完全按普通java代码写即可。只要改几个地方:
|
||||
|
||||
1. 类名,因为类名涉及import,所以一般用类的全名,可以有效规避import的问题,不用import类。
|
||||
|
||||
2.入参写法,入参要使用**`$1, $2...`**代表方法的第 1、2... 个参数
|
||||
|
||||
其他的特殊符号还包括:
|
||||
|
||||

|
||||
|
||||
编写完成后,覆盖方法的时候,有几种常见覆盖方法:
|
||||
|
||||

|
||||
|
||||
无论是哪个方法,多行代码的内容前后都要添加大括号
|
||||
|
||||

|
||||
|
||||
总结:简单的修改jar包内容,在缺失源码的情况下想修改某个功能时,可以很好的派上用场
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
1.@GetMapping: 参数可以放置在 header,地址【query string】。
|
||||
|
||||
(1)用来指定web端必须以GET请求方式访问。
|
||||
|
||||
(2)用@GetMapping注解的方法,无论方法参数类型是常用数据类型,还是自定义引用数据类型,
|
||||
|
||||
在没有使用@RequestHeader注解的情况下,参数都是拼接在地址里面去的【query string】。
|
||||
|
||||
(3)使用@RequestHeader注解方法参数时,参数必须放置在请求头【header】里去。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2.@RequestHeader:指定方法的参数是从请求的请求头【header】里取的。注意: @RequestHeader只能
|
||||
|
||||
注解常用数据类型【一般是string】,不能注解引用数据类型
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3.@PostMapping:参数可以放置在地址栏【query string】,header,请求体里。
|
||||
|
||||
(1)用来指定web端必须以POST请求方式访问。
|
||||
|
||||
(2)@PostMapping注解方法时,web端可以从header,地址【query string】,body里向后端传递参数。
|
||||
|
||||
(3)在方法参数里没有指定参数被@RequestHeader,@RequestBody注解时。默认是从地址栏
|
||||
|
||||
取数据,同时:后台可以使用实体对象做方法形参从地址栏取参数。用@RequestHeader,
|
||||
|
||||
@RequestBody注解的参数分别是从请求头,请求体里取参数。
|
||||
|
||||
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
1.@GetMapping: 参数可以放置在 header,地址【query string】。
|
||||
|
||||
(1)用来指定web端必须以GET请求方式访问。
|
||||
|
||||
(2)用@GetMapping注解的方法,无论方法参数类型是常用数据类型,还是自定义引用数据类型,
|
||||
|
||||
在没有使用@RequestHeader注解的情况下,参数都是拼接在地址里面去的【query string】。
|
||||
|
||||
(3)使用@RequestHeader注解方法参数时,参数必须放置在请求头【header】里去。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2.@RequestHeader:指定方法的参数是从请求的请求头【header】里取的。注意: @RequestHeader只能
|
||||
|
||||
注解常用数据类型【一般是string】,不能注解引用数据类型
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3.@PostMapping:参数可以放置在地址栏【query string】,header,请求体里。
|
||||
|
||||
(1)用来指定web端必须以POST请求方式访问。
|
||||
|
||||
(2)@PostMapping注解方法时,web端可以从header,地址【query string】,body里向后端传递参数。
|
||||
|
||||
(3)在方法参数里没有指定参数被@RequestHeader,@RequestBody注解时。默认是从地址栏
|
||||
|
||||
取数据,同时:后台可以使用实体对象做方法形参从地址栏取参数。用@RequestHeader,
|
||||
|
||||
@RequestBody注解的参数分别是从请求头,请求体里取参数。
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
**[LibreSpeed](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=LibreSpeed&zhida_source=entity)** -- 免费开源的在线测速工具;
|
||||
|
||||
**[123APPS](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=123APPS&zhida_source=entity)** -- 免费好用的在线音视频编辑、PDF 处理工具(在线工具箱);
|
||||
|
||||
**[d1tools TTS](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=d1tools+TTS&zhida_source=entity)** -- 免费的在线文字转语音工具,支持74种语言、318个主播声音;
|
||||
|
||||
**[LANDrop](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=LANDrop&zhida_source=entity)** -- 免费、跨平台的局域网文件传输工具;
|
||||
|
||||
**[Caesium](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=Caesium&zhida_source=entity)** -- 支持批量处理,免费开源的图片压缩软件;
|
||||
|
||||
**[洋芋田图像工具箱](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=%E6%B4%8B%E8%8A%8B%E7%94%B0%E5%9B%BE%E5%83%8F%E5%B7%A5%E5%85%B7%E7%AE%B1&zhida_source=entity)** -- 为摄影、设计行业而生,免费开源,图像处理系列工具集;
|
||||
|
||||
**Mp3tag** -- 易用免费、功能强大的音频文件元数据编辑工具,优雅、规范地管理本地音乐文件;
|
||||
|
||||
**[Shot Easy](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=Shot+Easy&zhida_source=entity)** -- 免费好用的轻量级图片编辑 / 美化 / 压缩 / 抠图工具;
|
||||
|
||||
**[妙言](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=%E5%A6%99%E8%A8%80&zhida_source=entity)** -- mac平台,轻量简洁、免费开源的高颜值markdown编辑器;
|
||||
|
||||
**[Readest](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=Readest&zhida_source=entity)** -- 开源免费、界面精美,功能丰富,支持格式众多,且简单易用,支持在线翻译的Windows端电子书阅读器;
|
||||
|
||||
**[RustDesk](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=RustDesk&zhida_source=entity)** -- 免费流畅不限速的远程控制软件;
|
||||
|
||||
**[HEU KMS Activator](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=HEU+KMS+Activator&zhida_source=entity)** -- Windows系统与Office激活工具;
|
||||
|
||||
**[云萌](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=%E4%BA%91%E8%90%8C&zhida_source=entity)** -- Windows系统与Office激活工具;
|
||||
|
||||
**[Cyberduck](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=Cyberduck&zhida_source=entity)** -- 跨平台的开源云存储服务聚合客户端;
|
||||
|
||||
**[ShareX](https://zhida.zhihu.com/search?content_id=708955313&content_type=Answer&match_order=1&q=ShareX&zhida_source=entity)** -- 免费开源截图工具;
|
||||
|
||||
**AnyTXT Searcher** -- 强大的本地文本文件内容搜索工具;
|
||||
@@ -0,0 +1,926 @@
|
||||
|
||||
## **前言**
|
||||
|
||||
前一段时间,有同事使用ThreadLocal踩坑了,正好引起了我的兴趣。
|
||||
|
||||
所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西还真不少。
|
||||
|
||||
我把精华浓缩了一下,汇集成了下面11个问题,看看你能顶住第几个?
|
||||
|
||||

|
||||
|
||||
最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
|
||||
|
||||
|
||||
|
||||
## **1. 为什么要用ThreadLocal?**
|
||||
|
||||
并发编程是一项非常重要的技术,它让我们的程序变得更加高效。
|
||||
|
||||
但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。
|
||||
|
||||
为了解决线程安全问题,JDK出现了很多技术手段,比如:使用synchronized或Lock,给访问公共资源的代码上锁,保证了代码的原子性。
|
||||
|
||||
但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。
|
||||
|
||||
因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。
|
||||
|
||||
它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
@Service
|
||||
public class ThreadLocalService {
|
||||
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
|
||||
|
||||
public void add() {
|
||||
threadLocal.set(1);
|
||||
doSamething();
|
||||
Integer integer = threadLocal.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
## **2. ThreadLocal的原理是什么?**
|
||||
|
||||
为了搞清楚ThreadLocal的底层实现原理,我们不得不扒一下源码。
|
||||
|
||||
ThreadLocal的内部有一个静态的内部类叫:ThreadLocalMap。
|
||||
|
||||
```text
|
||||
public class ThreadLocal<T> {
|
||||
...
|
||||
public T get() {
|
||||
//获取当前线程
|
||||
Thread t = Thread.currentThread();
|
||||
//获取当前线程的成员变量ThreadLocalMap对象
|
||||
ThreadLocalMap map = getMap(t);
|
||||
if (map != null) {
|
||||
//根据threadLocal对象从map中获取Entry对象
|
||||
ThreadLocalMap.Entry e = map.getEntry(this);
|
||||
if (e != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
//获取保存的数据
|
||||
T result = (T)e.value;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
//初始化数据
|
||||
return setInitialValue();
|
||||
}
|
||||
|
||||
private T setInitialValue() {
|
||||
//获取要初始化的数据
|
||||
T value = initialValue();
|
||||
//获取当前线程
|
||||
Thread t = Thread.currentThread();
|
||||
//获取当前线程的成员变量ThreadLocalMap对象
|
||||
ThreadLocalMap map = getMap(t);
|
||||
//如果map不为空
|
||||
if (map != null)
|
||||
//将初始值设置到map中,key是this,即threadLocal对象,value是初始值
|
||||
map.set(this, value);
|
||||
else
|
||||
//如果map为空,则需要创建新的map对象
|
||||
createMap(t, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public void set(T value) {
|
||||
//获取当前线程
|
||||
Thread t = Thread.currentThread();
|
||||
//获取当前线程的成员变量ThreadLocalMap对象
|
||||
ThreadLocalMap map = getMap(t);
|
||||
//如果map不为空
|
||||
if (map != null)
|
||||
//将值设置到map中,key是this,即threadLocal对象,value是传入的value值
|
||||
map.set(this, value);
|
||||
else
|
||||
//如果map为空,则需要创建新的map对象
|
||||
createMap(t, value);
|
||||
}
|
||||
|
||||
static class ThreadLocalMap {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。
|
||||
|
||||
其中ThreadLocalMap类的内部如下:
|
||||
|
||||
```text
|
||||
static class ThreadLocalMap {
|
||||
static class Entry extends WeakReference<ThreadLocal<?>> {
|
||||
Object value;
|
||||
|
||||
Entry(ThreadLocal<?> k, Object v) {
|
||||
super(k);
|
||||
value = v;
|
||||
}
|
||||
}
|
||||
...
|
||||
private Entry[] table;
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。
|
||||
|
||||
ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value。
|
||||
|
||||
而ThreadLocalMap被定义成了Thread类的成员变量。
|
||||
|
||||
```text
|
||||
public class Thread implements Runnable {
|
||||
...
|
||||
ThreadLocal.ThreadLocalMap threadLocals = null;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
下面用一张图从宏观上,认识一下ThreadLocal的整体结构:
|
||||
|
||||

|
||||
|
||||
从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。
|
||||
|
||||
Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。
|
||||
|
||||
下面用一张图总结一下引用关系:
|
||||
|
||||

|
||||
|
||||
上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
|
||||
|
||||
需要特别说明的是,上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。
|
||||
|
||||
## **3. 为什么用ThreadLocal做key?**
|
||||
|
||||
不知道你有没有思考过这样一个问题:ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
|
||||
|
||||
如果在你的应用中,一个线程中只使用了一个ThreadLocal对象,那么使用Thread做key也未尝不可。
|
||||
|
||||
```text
|
||||
@Service
|
||||
public class ThreadLocalService {
|
||||
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
但实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就出有问题?
|
||||
|
||||
```text
|
||||
@Service
|
||||
public class ThreadLocalService {
|
||||
private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
假如使用Thread做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么知道要获取哪个ThreadLocal对象呢?
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法,轻松获取到你想要的ThreadLocal对象。
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## **4. Entry的key为什么设计成弱引用?**
|
||||
|
||||
前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。
|
||||
|
||||
那么,为什么要这样设计呢?
|
||||
|
||||
假如key对ThreadLocal对象的弱引用,改为强引用。
|
||||
|
||||

|
||||
|
||||
我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。
|
||||
|
||||
即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。
|
||||
|
||||
此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。
|
||||
|
||||
就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
|
||||
|
||||
那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
|
||||
|
||||
为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用。
|
||||
|
||||
> 弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。
|
||||
|
||||
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
**接下来,最关键的地方来了。**
|
||||
|
||||
由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的get、set或remove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。
|
||||
|
||||
此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。
|
||||
|
||||
如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
|
||||
|
||||
这样就能最大程度的解决内存泄露问题。
|
||||
|
||||
需要特别注意的地方是:
|
||||
|
||||
1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?
|
||||
2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。
|
||||
|
||||
下面看看弱引用的例子:
|
||||
|
||||
```text
|
||||
public static void main(String[] args) {
|
||||
WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
|
||||
System.out.println(weakReference0.get());
|
||||
System.gc();
|
||||
System.out.println(weakReference0.get());
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
打印结果:
|
||||
|
||||
```text
|
||||
java.lang.Object@1ef7fe8e
|
||||
null
|
||||
|
||||
|
||||
```
|
||||
|
||||
传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。
|
||||
|
||||
但如果出现下面这种情况:
|
||||
|
||||
```text
|
||||
public static void main(String[] args) {
|
||||
Object object = new Object();
|
||||
WeakReference<Object> weakReference1 = new WeakReference<>(object);
|
||||
System.out.println(weakReference1.get());
|
||||
System.gc();
|
||||
System.out.println(weakReference1.get());
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```text
|
||||
java.lang.Object@1ef7fe8e
|
||||
java.lang.Object@1ef7fe8e
|
||||
|
||||
|
||||
```
|
||||
|
||||
先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。
|
||||
|
||||
我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。
|
||||
|
||||
如果将object强引用设置为null:
|
||||
|
||||
```text
|
||||
public static void main(String[] args) {
|
||||
Object object = new Object();
|
||||
WeakReference<Object> weakReference1 = new WeakReference<>(object);
|
||||
System.out.println(weakReference1.get());
|
||||
System.gc();
|
||||
System.out.println(weakReference1.get());
|
||||
|
||||
object=null;
|
||||
System.gc();
|
||||
System.out.println(weakReference1.get());
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```text
|
||||
java.lang.Object@6f496d9f
|
||||
java.lang.Object@6f496d9f
|
||||
null
|
||||
|
||||
|
||||
```
|
||||
|
||||
第二次gc之后,弱引用能够被正常回收。
|
||||
|
||||
> 由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。
|
||||
|
||||
此外,你可能还会问这样一个问题:**Entry的value为什么不设计成弱引用?**
|
||||
|
||||
答:Entry的value假如只是被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。
|
||||
|
||||
而相比之下,Entry的key,管理的地方就非常明确了。
|
||||
|
||||
这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。
|
||||
|
||||
## **5. ThreadLocal真的会导致内存泄露?**
|
||||
|
||||
通过上面的Entry对象中的key设置成弱引用,并且使用get、set或remove方法清理key为null的value值,就能彻底解决内存泄露问题?
|
||||
|
||||
答案是否定的。
|
||||
|
||||
如下图所示:
|
||||
|
||||

|
||||
|
||||
假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。
|
||||
|
||||
那么,Entry的value值一直都没被清空。
|
||||
|
||||
所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。
|
||||
|
||||
其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露。
|
||||
|
||||
## **6. 如何解决内存泄露问题?**
|
||||
|
||||
前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?
|
||||
|
||||
答:有办法,调用ThreadLocal对象的remove方法。
|
||||
|
||||
不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:
|
||||
|
||||
先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。
|
||||
|
||||
```text
|
||||
public class CurrentUser {
|
||||
private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
|
||||
|
||||
public static void set(UserInfo userInfo) {
|
||||
THREA_LOCAL.set(userInfo);
|
||||
}
|
||||
|
||||
public static UserInfo get() {
|
||||
THREA_LOCAL.get();
|
||||
}
|
||||
|
||||
public static void remove() {
|
||||
THREA_LOCAL.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
然后在业务代码中调用相关方法:
|
||||
|
||||
```text
|
||||
public void doSamething(UserDto userDto) {
|
||||
UserInfo userInfo = convert(userDto);
|
||||
|
||||
try{
|
||||
CurrentUser.set(userInfo);
|
||||
...
|
||||
|
||||
//业务代码
|
||||
UserInfo userInfo = CurrentUser.get();
|
||||
...
|
||||
} finally {
|
||||
CurrentUser.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。
|
||||
|
||||
remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
|
||||
|
||||
## **7. ThreadLocal是如何定位数据的?**
|
||||
|
||||
前面说过ThreadLocalMap对象底层是用Entry数组保存数据的。
|
||||
|
||||
那么问题来了,ThreadLocal是如何定位Entry数组数据的?
|
||||
|
||||
在ThreadLocal的get、set、remove方法中都有这样一行代码:
|
||||
|
||||
```text
|
||||
int i = key.threadLocalHashCode & (len-1);
|
||||
|
||||
|
||||
```
|
||||
|
||||
通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。
|
||||
|
||||
这是一种hash算法。
|
||||
|
||||
接下来给大家举个例子:假设len=16,key.threadLocalHashCode=31,
|
||||
|
||||
于是: int i = 31 & 15 = 15
|
||||
|
||||
相当于:int i = 31 % 16 = 15
|
||||
|
||||
计算的结果是一样的,但是使用与运算效率跟高一些。
|
||||
|
||||
为什么与运算效率更高?
|
||||
|
||||
答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。
|
||||
|
||||
如果使用hash算法定位具体位置的话,就可能会出现hash冲突的情况,即两个不同的hashCode取模后的值相同。
|
||||
|
||||
ThreadLocal是如何解决hash冲突的呢?
|
||||
|
||||
我们看看getEntry是怎么做的:
|
||||
|
||||
```text
|
||||
private Entry getEntry(ThreadLocal<?> key) {
|
||||
//通过hash算法获取下标值
|
||||
int i = key.threadLocalHashCode & (table.length - 1);
|
||||
Entry e = table[i];
|
||||
//如果下标位置上的key正好是我们所需要寻找的key
|
||||
if (e != null && e.get() == key)
|
||||
//说明找到数据了,直接返回
|
||||
return e;
|
||||
else
|
||||
//说明出现hash冲突了,继续往后找
|
||||
return getEntryAfterMiss(key, i, e);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
再看看getEntryAfterMiss方法:
|
||||
|
||||
```text
|
||||
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
|
||||
Entry[] tab = table;
|
||||
int len = tab.length;
|
||||
|
||||
//判断Entry对象如果不为空,则一直循环
|
||||
while (e != null) {
|
||||
ThreadLocal<?> k = e.get();
|
||||
//如果当前Entry的key正好是我们所需要寻找的key
|
||||
if (k == key)
|
||||
//说明这次真的找到数据了
|
||||
return e;
|
||||
if (k == null)
|
||||
//如果key为空,则清理脏数据
|
||||
expungeStaleEntry(i);
|
||||
else
|
||||
//如果还是没找到数据,则继续往后找
|
||||
i = nextIndex(i, len);
|
||||
e = tab[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
关键看看nextIndex方法:
|
||||
|
||||
```text
|
||||
private static int nextIndex(int i, int len) {
|
||||
return ((i + 1 < len) ? i + 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。
|
||||
|
||||
寻找的大致过程如下图所示:
|
||||
|
||||

|
||||
|
||||
如果找到最后一个,还是没有找到,则再从头开始找。
|
||||
|
||||

|
||||
|
||||
不知道你有没有发现,它构成了一个:环形。
|
||||
|
||||
ThreadLocal从数组中找数据的过程大致是这样的:
|
||||
|
||||
1. 通过key的hashCode取余计算出一个下标。
|
||||
2. 通过下标,在数组中定位具体Entry,如果key正好是我们所需要的key,说明找到了,则直接返回数据。
|
||||
3. 如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。
|
||||
4. 如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。
|
||||
5. 如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。
|
||||
6. 直到找到第一个Entry为空为止。
|
||||
|
||||
## **8. ThreadLocal是如何扩容的?**
|
||||
|
||||
从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?
|
||||
|
||||
在set方法中会调用rehash方法:
|
||||
|
||||
```text
|
||||
private void set(ThreadLocal<?> key, Object value) {
|
||||
Entry[] tab = table;
|
||||
int len = tab.length;
|
||||
int i = key.threadLocalHashCode & (len-1);
|
||||
|
||||
for (Entry e = tab[i];
|
||||
e != null;
|
||||
e = tab[i = nextIndex(i, len)]) {
|
||||
ThreadLocal<?> k = e.get();
|
||||
|
||||
if (k == key) {
|
||||
e.value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (k == null) {
|
||||
replaceStaleEntry(key, value, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tab[i] = new Entry(key, value);
|
||||
int sz = ++size;
|
||||
if (!cleanSomeSlots(i, sz) && sz >= threshold)
|
||||
rehash();
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
注意一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash方法。
|
||||
|
||||
threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:
|
||||
|
||||
```text
|
||||
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
|
||||
table = new Entry[INITIAL_CAPACITY];
|
||||
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
|
||||
table[i] = new Entry(firstKey, firstValue);
|
||||
size = 1;
|
||||
setThreshold(INITIAL_CAPACITY);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
调用setThreshold方法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16。
|
||||
|
||||
```text
|
||||
private void setThreshold(int len) {
|
||||
threshold = len * 2 / 3;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10。
|
||||
|
||||
换句话说当sz大于等于10时,就可以考虑扩容了。
|
||||
|
||||
rehash代码如下:
|
||||
|
||||
```text
|
||||
private void rehash() {
|
||||
//先尝试回收一次key为null的值,腾出一些空间
|
||||
expungeStaleEntries();
|
||||
|
||||
if (size >= threshold - threshold / 4)
|
||||
resize();
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。
|
||||
|
||||
如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。
|
||||
|
||||
计算公式如下:
|
||||
|
||||
```text
|
||||
16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8
|
||||
|
||||
|
||||
```
|
||||
|
||||
也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。
|
||||
|
||||
```text
|
||||
private void resize() {
|
||||
Entry[] oldTab = table;
|
||||
int oldLen = oldTab.length;
|
||||
//按2倍的大小扩容
|
||||
int newLen = oldLen * 2;
|
||||
Entry[] newTab = new Entry[newLen];
|
||||
int count = 0;
|
||||
|
||||
for (int j = 0; j < oldLen; ++j) {
|
||||
Entry e = oldTab[j];
|
||||
if (e != null) {
|
||||
ThreadLocal<?> k = e.get();
|
||||
if (k == null) {
|
||||
e.value = null; // Help the GC
|
||||
} else {
|
||||
int h = k.threadLocalHashCode & (newLen - 1);
|
||||
while (newTab[h] != null)
|
||||
h = nextIndex(h, newLen);
|
||||
newTab[h] = e;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setThreshold(newLen);
|
||||
size = count;
|
||||
table = newTab;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
resize中每次都是按2倍的大小扩容。
|
||||
|
||||
扩容的过程如下图所示:
|
||||
|
||||

|
||||
|
||||
扩容的关键步骤如下:
|
||||
|
||||
1. 老size + 1 = 新size
|
||||
2. 如果新size大于等于老size的2/3时,需要考虑扩容。
|
||||
3. 扩容前先尝试回收一次key为null的值,腾出一些空间。
|
||||
4. 如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
|
||||
5. 每次都是按2倍的大小扩容。
|
||||
|
||||
## **9. 父子线程如何共享数据?**
|
||||
|
||||
前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。
|
||||
|
||||
但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
public class ThreadLocalTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
|
||||
threadLocal.set(6);
|
||||
System.out.println("父线程获取数据:" + threadLocal.get());
|
||||
|
||||
new Thread(() -> {
|
||||
System.out.println("子线程获取数据:" + threadLocal.get());
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```text
|
||||
父线程获取数据:6
|
||||
子线程获取数据:null
|
||||
|
||||
|
||||
```
|
||||
|
||||
你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。
|
||||
|
||||
显然通过ThreadLocal,无法在父子线程中共享数据。
|
||||
|
||||
那么,该怎么办呢?
|
||||
|
||||
答:使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
|
||||
|
||||
修改代码之后:
|
||||
|
||||
```text
|
||||
public class ThreadLocalTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
|
||||
threadLocal.set(6);
|
||||
System.out.println("父线程获取数据:" + threadLocal.get());
|
||||
|
||||
new Thread(() -> {
|
||||
System.out.println("子线程获取数据:" + threadLocal.get());
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```text
|
||||
父线程获取数据:6
|
||||
子线程获取数据:6
|
||||
|
||||
|
||||
```
|
||||
|
||||
果然,在换成InheritableThreadLocal之后,在子线程中能够正常获取父线程中设置的值。
|
||||
|
||||
其实,在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。
|
||||
|
||||
Thread类的部分代码如下:
|
||||
|
||||
```text
|
||||
ThreadLocal.ThreadLocalMap threadLocals = null;
|
||||
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
|
||||
|
||||
|
||||
```
|
||||
|
||||
最关键的一点是,在它的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。
|
||||
|
||||
感兴趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面还会有专栏。
|
||||
|
||||
## **10. 线程池中如何共享数据?**
|
||||
|
||||
在真实的业务场景中,一般很少用单独的线程,绝大多数,都是用的线程池。
|
||||
|
||||
那么,在线程池中如何共享ThreadLocal对象生成的数据呢?
|
||||
|
||||
因为涉及到不同的线程,如果直接使用ThreadLocal,显然是不合适的。
|
||||
|
||||
我们应该使用InheritableThreadLocal,具体代码如下:
|
||||
|
||||
```text
|
||||
private static void fun1() {
|
||||
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
|
||||
threadLocal.set(6);
|
||||
System.out.println("父线程获取数据:" + threadLocal.get());
|
||||
|
||||
ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
|
||||
threadLocal.set(6);
|
||||
executorService.submit(() -> {
|
||||
System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
|
||||
});
|
||||
|
||||
threadLocal.set(7);
|
||||
executorService.submit(() -> {
|
||||
System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```text
|
||||
父线程获取数据:6
|
||||
第一次从线程池中获取数据:6
|
||||
第二次从线程池中获取数据:6
|
||||
|
||||
|
||||
```
|
||||
|
||||
由于这个例子中使用了单例线程池,固定线程数是1。
|
||||
|
||||
第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到,在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。
|
||||
|
||||
之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。
|
||||
|
||||
因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。
|
||||
|
||||
那么,这该怎么办呢?
|
||||
|
||||
答:使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。
|
||||
|
||||
可以通过如下pom文件引入该jar包:
|
||||
|
||||
```text
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
<version>2.11.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
```
|
||||
|
||||
代码调整如下:
|
||||
|
||||
```text
|
||||
private static void fun2() throws Exception {
|
||||
TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
|
||||
threadLocal.set(6);
|
||||
System.out.println("父线程获取数据:" + threadLocal.get());
|
||||
|
||||
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
|
||||
|
||||
threadLocal.set(6);
|
||||
ttlExecutorService.submit(() -> {
|
||||
System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
|
||||
});
|
||||
|
||||
threadLocal.set(7);
|
||||
ttlExecutorService.submit(() -> {
|
||||
System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```text
|
||||
父线程获取数据:6
|
||||
第一次从线程池中获取数据:6
|
||||
第二次从线程池中获取数据:7
|
||||
|
||||
|
||||
```
|
||||
|
||||
我们看到,使用了TransmittableThreadLocal之后,第二次从线程中也能正确获取最新的数据7了。
|
||||
|
||||
nice。
|
||||
|
||||
如果你仔细观察这个例子,你可能会发现,代码中除了使用TransmittableThreadLocal类之外,还使用了TtlExecutors.getTtlExecutorService方法,去创建ExecutorService对象。
|
||||
|
||||
这是非常重要的地方,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。
|
||||
|
||||
创建ExecutorService对象,底层的submit方法会TtlRunnable或TtlCallable对象。
|
||||
|
||||
以TtlRunnable类为例,它实现了Runnable接口,同时还实现了它的run方法:
|
||||
|
||||
```text
|
||||
public void run() {
|
||||
Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
|
||||
if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
|
||||
Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
|
||||
|
||||
try {
|
||||
this.runnable.run();
|
||||
} finally {
|
||||
TransmittableThreadLocal.restoreBackup(backup);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("TTL value reference is released after run!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
这段代码的主要逻辑如下:
|
||||
|
||||
1. 把当时的ThreadLocal做个备份,然后将父类的ThreadLocal拷贝过来。
|
||||
2. 执行真正的run方法,可以获取到父类最新的ThreadLocal数据。
|
||||
3. 从备份的数据中,恢复当时的ThreadLocal数据。
|
||||
|
||||
## **11. ThreadLocal有哪些用途?**
|
||||
|
||||
最后,一起聊聊ThreadLocal有哪些用途?
|
||||
|
||||
老实说,使用ThreadLocal的场景挺多的。
|
||||
|
||||
下面列举几个常见的场景:
|
||||
|
||||
1. 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
|
||||
2. 在hiberate中管理session。
|
||||
3. 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
|
||||
4. 获取当前登录用户上下文。
|
||||
5. 临时保存权限数据。
|
||||
6. 使用MDC保存日志信息。
|
||||
|
||||
等等,还有很多业务场景,这里就不一一列举了。
|
||||
|
||||
由于篇幅有限,今天的内容先分享到这里。希望你看了这篇文章,会有所收获。
|
||||
|
||||
接下来留几个问题给大家思考一下:
|
||||
|
||||
1. ThreadLocal变量为什么建议要定义成static的?
|
||||
2. Entry数组为什么要通过hash算法计算下标,即直线寻址法,而不直接使用下标值?
|
||||
3. 强引用和弱引用有什么区别?
|
||||
4. Entry数组大小,为什么是2的N次方?
|
||||
5. 使用InheritableThreadLocal时,如果父线程中重新set值,在子线程中能够正确的获取修改后的新值吗?
|
||||
@@ -0,0 +1,16 @@
|
||||
如果用的logback实现,就用
|
||||
|
||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory
|
||||
.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
|
||||
root.setLevel(Level.INFO);
|
||||
1
|
||||
2
|
||||
3
|
||||
如果用的是log4j实现,就用
|
||||
|
||||
```Plain Text
|
||||
org.apache.log4j.Logger.getRootLogger().setLevel(Level.INFO);
|
||||
BasicConfigurator.configure(new ConsoleAppender(
|
||||
new ColorPatternLayout("%d{MM-dd HH:mm:ss} [%-25t] %-5p: %m%n")));
|
||||
```
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
如果用的logback实现,就用
|
||||
|
||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory
|
||||
.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
|
||||
root.setLevel(Level.INFO);
|
||||
1
|
||||
2
|
||||
3
|
||||
如果用的是log4j实现,就用
|
||||
|
||||
```Plain Text
|
||||
org.apache.log4j.Logger.getRootLogger().setLevel(Level.INFO);
|
||||
BasicConfigurator.configure(new ConsoleAppender(
|
||||
new ColorPatternLayout("%d{MM-dd HH:mm:ss} [%-25t] %-5p: %m%n")));
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
| | | | | | | |
|
||||
| ---- | --------- | ------ | ---------- | ---------- | ---- | ---- |
|
||||
| 任务模式 | Task Name | 工期 | 开始时间 | 完成时间 | 前置任务 | 资源名称 |
|
||||
| 自动计划 | 测试执行 | 5 days | 2025年1月8日 | 2025年1月14日 | | |
|
||||
| 手动计划 | 性能测试 | 1 day | 2025年1月8日 | 2025年1月8日 | | 陆世钰 |
|
||||
| 手动计划 | 云机性能测试 | 1 day | 2025年1月9日 | 2025年1月9日 | | 陆世钰 |
|
||||
| 手动计划 | 稳定性测试 | 3 days | 2025年1月10日 | 2025年1月14日 | | 陆世钰 |
|
||||
@@ -0,0 +1,81 @@
|
||||
项目进度计划管理在项目管理中至关重要,项目进度计划的制定应分为以下四步:
|
||||
|
||||
## 第一步:了解项目目标范围等基本情况
|
||||
>对应禅道中的需求管理、人员安排、阶段规划等
|
||||
|
||||
明确项目成本;
|
||||
明确项目实施范围;
|
||||
明确项目实施周期要求;**就是时间**
|
||||
明确项目投入人员情况;包括设备。**就是资源**
|
||||
明确项目合同中收款及验收情况。
|
||||
|
||||
## 第二步:项目制约因素分析,策划最优项目实施计划
|
||||
>对应禅道中的任务安排、测试管理、问题风险管理等
|
||||
|
||||
分析项目在本年度能否上线及验收;
|
||||
分析项目在本年度能否收 60%的款项(上线款);
|
||||
本年度能收 60%款项(上线款)不能完成验收的项目,验收计划需控制在第二年一季度或上半年;
|
||||
项目本年度必须验收(考虑压缩周期,增加人力,成本是否可控);
|
||||
了解客户在项目上的紧迫程度,需求人员支持情况等;
|
||||
了解项目标准的实施周期,投入人数;
|
||||
考虑上线到验收的时长,即试运行周期;
|
||||
分析项目实施风险、规避方案、解决措施等;
|
||||
了解测试范围,测试轮次,测试严格程度等。
|
||||
|
||||
## 第三步:项目实施计划制定
|
||||
>直接参照模板,在禅道中各个阶段创建关键任务
|
||||
|
||||
基于项目基本情况和制约因素确定项目的实施周期,先初步规划项目重要里程碑,再列出各里程碑阶段的关键事项,最后对关键事项分解形成具体任务,通过合理的排期形成项目实施计划,各阶段关键事项如下:
|
||||
|
||||
## 第四步:项目实施计划评审
|
||||
【对应禅道中的评审——项目计划】
|
||||
邀请项目总监,区域负责人一起评审;
|
||||
针对项目实施计划合理性,成本可控性,人员投入情况进行审核;
|
||||
针对项目实施计划风险,解决措施,规避方案,保障计划有效落地进行审核。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
产品型项目需要关联产品
|
||||
![[a5ad93c5bc48c150209b4aee3433454.png]]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
用户在互联网端的应用客户端运行在各种环境里,包括PC端Windows与Mac系统等、移动终端Android与iOS系统等,而传统的数字签名方案一般都是用USBKey或者BluetoothKey等硬件数字证书应用方案,由此带来的问题是不便捷、兼容性差、用户采购成本高。所以传统的数字证书应用方案已经不能适应当前密码安全的发展需求。
|
||||
|
||||
因此在用户无需任何额外硬件介质的条件下,为用户在互联网端提供安全合规的数字签名解决方案,有助于提升信雅达安全产品的综合竞争力,同时也有利于拓展非金融领域的安全产品市场,所以开发一套协同签名安全服务平台有着非常重要的意义。
|
||||
|
||||
支持PC端的Windows、Linux客户端,移动端的Android、 IOS系统的客户端,
|
||||
|
||||
客户端协同签名安全模块SDK:
|
||||
|
||||
1) 客户端协同签名安全模块 SDK 兼容 WEB。
|
||||
|
||||
2) 客户端协同签名安全模块 SDK 兼容文件操作
|
||||
|
||||
3) 客户端协同签名安全模块 SDK 兼容HMAC-SM3
|
||||
|
||||
4) 客户端协同签名安全模块 SDK 兼容HTTPS协议
|
||||
|
||||
协同签名安全服务平台:
|
||||
|
||||
5) 协同签名安全服务平台兼容信创数据库OceanBase?
|
||||
|
||||
6) 协同签名安全服务平台兼容HTTPS协议;
|
||||
|
||||
7) 协同签名安全服务平台web管理新增客户端证书存储、查询、删除
|
||||
|
||||
8) 协同签名安全服务平台web管理新增证书最大下载书配置,证书CA信息配置
|
||||
|
||||
9) 认证模块配置
|
||||
|
||||
10) 协同签名安全服务平台web管理支持非对称密钥的产生、分发、更换、使用、备份、恢复、销毁
|
||||
|
||||
11) 支持SNMP、SYSLOG等标准协议配置
|
||||
|
||||
12) 服务端支持keepalive功能
|
||||
|
||||
Reference in New Issue
Block a user