教程

在 Spring Boot 应用中设置默认时区(Timezone)

1、概览 有时,我们希望能够指定应用使用的时区。我们可以通过几种不同的方法来实现这一目标。一种方法是在执行应用时使用 JVM 参数。另一种方法是在启动生命周期的不同阶段以编程式在代码中进行更改。 本文将带你了解在 Spring Boot 应用中设置默认时区的几种方法。 2、主要概念 TimeZone 的默认值基于运行 JVM 的机器的操作系统。我们可以: 通过使用 user.timezone 参数传递 JVM 参数,可以根据运行任务或 JAR 的不同情况,以不同的方式传递参数。 在程序中使用 Bean 生命周期配置选项(在创建 Bean 时/创建 Bean 前),甚至在类内执行过程中使用这些选项。 在 Spring Boot 应用中设置默认 TimeZone 会影响不同的组件,如日志的时间戳、调度程序(Scheduler)、JPA/Hibernate 时间戳等。这意味着我们选择在何处执行取决于何时需要它生效。例如,是希望在创建某个 Bean 时生效,还是在初始化 WebApplicationContext 后生效? 精确地确定何时设置该值非常重要,因为这可能会导致不必要的应用行为。例如,警报服务可能会在时区更改生效前设置警报,从而导致警报在错误的时间启动。 在决定采用哪种方案之前,另一个需要考虑的因素是可测试性。使用 JVM 参数是比较简单的选择,但测试起来可能比较麻烦,也更容易出现错误。我们无法保证单元测试能以与生产部署相同的 JVM 参数运行。 3、设置 bootRun 任务的默认时区 如果使用 bootRun 任务运行应用,我们可以在命令行中使用 JVM 参数传递默认 TimeZone。 在这种情况下,我们设置的值从一开始执行就可用: mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Duser.timezone=Europe/Athens" 4、在执行 JAR 时设置默认时区 与运行 bootRun 任务类似,我们可以在执行 JAR 文件时在命令行中传递默认的 TimeZone 值。 同样,我们设置的值在执行之初就可用: java -Duser.timezone=Europe/Athens -jar spring-core-4-0.

解决 java.security.UnrecoverableKeyException: Cannot Recover Key

1、简介 本文将带你了解如 java.security.UnrecoverableKeyException 异常出现的原因以及如何解决该异常。 2、背景 在 Java 中,有一个 Keystore 的概念。它本质上是一个包含一些 secret 的文件。它可以包含证书链以及与之对应的私钥。由于证书只是一个带有公钥的 包装器,我们可以简单地说 Keystore 包含了一对非对称密钥。 通常,用密码(“password ”通 常也称为 “passphrase”)保护私钥是一种很好的做法。这不仅是 Java Keystore 的良好做法,也是网络安全的一般做法。实现这种保护的方法通常是使用对称密钥加密算法(如各种 AES 实例)对私钥和密码进行加密。 在这里对我们来说需要注意的是,Keystore 中的私钥可以使用密码进行加密,如上所述。这个特性并不是所有类型的 Keystore 都支持,例如,JKS Keystore 支持私钥密码保护,但 PKCS12 Keystore 不支持。在我们的示例中,我们需要密码保护功能,因此我们使用 JKS Keystore。 3、UnrecoverableKeyException java.security.UnrecoverableKeyException 通常发生在使用 KeyManagerFactory 时,特别是调用 init() 方法时。这是 JSSE 中的一个类,允许我们检索 KeyManager 实例。KeyManager 是一个接口,它代表了一个抽象概念,负责将我们作为客户端向对等方进行身份验证。 init() 方法需要两个参数 - 用于获取认证凭证的 Keystore 和用于私钥解密的密码。当 KeyManagerFactory 无法恢复证书链的私钥时,就会出现 java.security.UnrecoverableKeyException 异常。问题来了 - UnrecoverableKeyException 中的 “recover” 到底是什么意思?这意味着证书链的私钥无法用给定的密码解密。因此,java.security.UnrecoverableKeyException 最常见的原因是 Keystore 中的私钥密码错误。 总之,如果为 KeyManagerFactory 提供的私钥密码/口令不正确,那么 KeyManagerFactory 将无法解密密钥,因此会出现此异常。

Spring Boot 中 Spring Security 自动配置

1、概览 本文将带你了解 Spring Boot 中 Spring Security 的自动配置、默认安全配置,以及如何在需要时禁用或自定义它。 2、默认的 Spring Security 设置 首先添加 security starter 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> 这包含初始/默认 Security 配置的 SecurityAutoConfiguration 类。 这里没有指定版本,因为项目使用了 spring-boot-starter-parent 作为 parent。 默认情况下,应用会启用身份验证,内容协商(Content Negotiation)用于确定应使用 basic 还是 formLogin。 有一些预定义的配置属性: spring.security.user.name= spring.security.user.password= 如果不使用预定义属性 spring.security.user.password 配置密码并启动应用,默认密码将随机生成并打印在控制台日志中: Using default security password: c8be15da-4489-4491-9dc6-fab3f91435c7 有关更多默认值,请参阅 Spring Boot 中文文档中的 属性配置。 3、禁用自动配置 要禁止 Security 自动配置并添加我们的自定义配置,需要排除 SecurityAutoConfiguration 自动配置类。 可以通过 @SpringBootApplication 注解中的 exclude 属性来实现: @SpringBootApplication(exclude = { SecurityAutoConfiguration.class }) public class SpringBootSecurityApplication { public static void main(String[] args) { SpringApplication.

Spring Security 7 中的重大变化

虽然 Spring Security 7.0 尚未确定发布日期,但是我们还是需要提前做一些准备工作,因为在已知的信息中,在 Spring Security 7.0 中会有一大批大家熟悉的 API 被移除。这些 API 在 Spring Security 6 中已经处于废弃状态,但是还能用,但是到了 Spring Security 7.0,这些就被移除了,所以我们还是有必要来看看 Spring Security 7.0 中的一些比较典型的变化。 1、Lambda 配置 Lambda DSL 自 Spring Security 5.2 版本以来就存在,它允许使用 lambda 表达式配置 HTTP Security。 我们来看看使用 lambda 配置 HTTP 安全性与之前的配置风格相比有何差别: 使用 lambda 的配置 @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/blog/**").permitAll() .anyRequest().authenticated() ) .formLogin(formLogin -> formLogin .loginPage("/login") .

Spring Data JPA 异常 “IllegalArgumentException: Not a Managed Type”

1、概览 使用 Spring Data JPA 时,应用启动出现异常。大致如下: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerAdapter' ... Caused by: java.lang.IllegalArgumentException: Not a managed type: ...OurEntity at org.hibernate.metamodel.internal.MetamodelImpl.managedType(MetamodelImpl.java:583) at org.hibernate.metamodel.internal.MetamodelImpl.managedType(MetamodelImpl.java:85) ... 大意是说,一些 Bean 创建失败了,导致应用启动失败。 根异常是 IllegalArgumentException:“Not a managed type”,本文将带你了解出现这个异常的原因,以及如何解决该异常。 2、缺少 @Entity 注解 出现这种异常的一个可能原因是,忘记使用 @Entity 注解来标记实体。 2.1、重现问题 假设有以下实体类: public class EntityWithoutAnnotation { @Id private Long id; } 及其对应的 Spring Data JPA repository: public interface EntityWithoutAnnotationRepository extends JpaRepository<EntityWithoutAnnotation, Long> { } 最后是 Application 启动类,它会扫描上面定义的所有类: @SpringBootApplication public class EntityWithoutAnnotationApplication { } 尝试使用此 Application 来启动 Spring Context:

JPA 中的 PersistenceUnit 和 PersistenceContext

1、概览 Persistence Context(持久化上下文)和 Persistence Unit(持久化单元)是 JPA 中的两个重要概念,用来管理应用中实体的生命周期。 本文将带你了解 JPA 中的 EntityManager(实体管理器) 的作用,以及 Persistence Context 和 Persistence Unit 的重要性和用例。 2、EntityManager 和 EntityManagerFactory 首先来看看 EntityManager 和 EntityManagerFactory 接口,它们在管理持久性(Persistence)、实体和数据库交互方面发挥着重要作用。 2.1、EntityManager EntityManager 是一个与 Persistence Context 交互的接口。它对实体执行 CRUD 操作、跟踪更改并确保在事务提交时与数据库同步。EntityManager 代表一个 Persistence Context,并在事务范围内运行。 2.2、EntityManagerFactory EntityManagerFactory 是一个创建 EntityManager 的接口,有效地发挥着工厂的作用。创建时,EntityManagerFactory 会与特定的 Persistence Unit 关联,从而创建 EntityManager 的实例。 3、PersistenceContext PersistenceContext 是一个短暂的、事务范围的上下文,用于管理实体的生命周期。它代表一组存储在内存中的 “托管实体”,是实体管理器的一级缓存。如果事务开始,就会创建持久化上下文,并最终在事务提交或回滚时关闭或清除。 持久化上下文会自动检测对托管实体所做的更改,并确保所有实体更改与持久化存储(Persistence Storage)同步。 我们可以使用 @PersistenceContext 注解定义持久化上下文(Persistence Context)的类型: @PersistenceContext private EntityManager entityManager; JPA 中有两种持久化上下文: TRANSACTION 和 EXTENDED。 首先,使用 @Entity 注解创建与 PRODUCT 表相对应的实体:

Spring Boot 使用 Grafana Loki 来收集和显示日志

1、简介 Grafana 实验室受 Prometheus 的启发开发了开源日志聚合系统 Loki。该系统的目的是存储日志数据并编制索引,从而方便高效地查询和分析由不同应用和系统生成的日志。 本文将带你了解如何在 Spring Boot 中使用 Loki 收集和汇总日志,并使用 Grafana 显示日志。 2、运行 Loki 和 Grafana 服务 首先以 Docker 容器的方式启动 Loki 和 Grafana 服务,以便收集和观察日志。 在 docker-compose 文件中定义 Loki 和 Grafana 服务: version: "3" networks: loki: services: loki: image: grafana/loki:2.9.0 ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml networks: - loki grafana: environment: - GF_PATHS_PROVISIONING=/etc/grafana/provisioning - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin entrypoint: - sh - -euc - | mkdir -p /etc/grafana/provisioning/datasources cat <<EOF > /etc/grafana/provisioning/datasources/ds.

使用 Spring REST Docs 生成 API 文档

1、概览 API 文档在团队开发中极其重要,特别是在 API 接口及其复杂的情况下,良好的 API 文档不仅能提升开发效率,还能显示产品的质量。如果一家公司的 API 文档写得马马虎虎,那么它的 API 也可能写得马马虎虎。 程序员都讨厌写自己文档和别人不写文档。 本文将带你了解如何使用 Spring REST Docs 自动地生成 API 文档。 2、API 接口 一个简单 API 如下: @RestController @RequestMapping("/books") public class BookController { private final BookService service; public BookController(BookService service) { this.service = service; } @GetMapping public List<Book> getBooks(@RequestParam(name = "page") Integer page) { return service.getBooks(page); } } 该 API 返回系统中的 Book 数据。不过,由于可用图书数量庞大,不可能一次性返回所有。所以给客户端提供了一个查询参数 page,用于控制数据分页。 而且,需要把该参数设置为必须参数(默认就是必须参数),避免客户端一次性请求过多数据。如果客户端未提交该查询参数,就会收到状态为 400 的错误提示。 3、文档 编写文档的通常方法是 “手写文档”,这意味着开发人员必须把同一件事写两遍。首先写代码,然后再写对应的文档。太麻烦! 文档是一种相当正式的文件,其目标是清晰明了,并不需要太多花里胡哨的东西。因此,我们可以根据代码生成文档,这样就不用重复写同样的东西,而且所有的改动都会反映在文档中。 我们可以通过 Spring REST Docs 来生成 API 文档。不过,它不是从代码中生成文档,因为代码没有提供太多上下文,而是从测试中生成文档。这可以表达相当复杂的案例和示例。另一个好处是,如果测试失败,文档也不会生成。

Spring Data JPA 实现 updateOrInsert(更新或保存)

1、简介 在应用开发中,执行 “更新或插入” 操作(也称为 “upsert”)的需要是很常见的。这个操作涉及将新记录存入数据库表中,如果记录不存在,则插入新记录;如果记录已经存在,则更新现有记录。 本文将带你了解使用 Spring Data JPA 执行 “更新或插入” 操作的不同方法。 2、实体类 定义 CreditCard 实体类用于演示: @Entity @Table(name="credit_card") public class CreditCard { @Id @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "credit_card_id_seq") @SequenceGenerator(name = "credit_card_id_seq", sequenceName = "credit_card_id_seq", allocationSize = 1) private Long id; private String cardNumber; private String expiryDate; private Long customerId; // Get / Set 方法省略 } 3、实现 本文介绍三种不同的方法来实现 “更新或插入”。 3.1、使用 Repository 方法 使用从 CrudRepository 接口继承的 save(entity) 方法在 Repository 中编写一个带事务的 default 方法。

Spring Boot 在测试时禁用 @Cacheable 缓存

1、简介 缓存是一种有效的策略,当执行结果在一段已知时间内没有变化时,可以避免重复执行逻辑,从而提高性能。 Spring Boot 提供了 @Cacheable 注解,可以在方法上定义该注解,它就会缓存方法的结果。在某些情况下,例如在测试环境中进行测试时,我们可能需要禁用缓存来观察某些修改后的行为。 本文将带你了解如何配置 Spring Boot 中的缓存,以及如何在需要时禁用缓存。 2、缓存配置 设置一个简单的用例,通过 ISBN(国际标准书号)查询图书评论,并在某个逻辑中使用 @Cacheable 对该方法返回的结果进行缓存。 实体类 BookReview 如下,它包含 bookRating、isbn 等信息: @Entity @Table(name="BOOK_REVIEWS") public class BookReview { @Id @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "book_reviews_reviews_id_seq") @SequenceGenerator(name = "book_reviews_reviews_id_seq", sequenceName = "book_reviews_reviews_id_seq", allocationSize = 1) private Long reviewsId; private String userId; private String isbn; private String bookRating; // Get / Set 方法省略 } 在 BookRepository 中添加一个简单的 findByIsbn() 方法,用于按 isbn 查询书评: public interface BookRepository extends JpaRepository<BookReview, Long> { List<BookReview> findByIsbn(String isbn); } BookReviewsLogic 类包含一个在 BookRepository 中调用 findByIsbn() 的方法。我们添加了 @Cacheable 注解,将指定 isbn 的结果缓存在 book_reviews 缓存中: