教程

Spring Data JPA 中的 getReferenceById() 和 findById() 方法

1、概览 JpaRepository 供了 CRUD 操作的基本方法。其中 getReferenceById(ID) 和 findById(ID) 是经常引起混淆的方法。这些方法是 getOne(ID)、findOne(ID) 和 getById(ID) 的新 API 名称。 本文将带你了解这些方法之间的区别,以及各自的适用场景。 2、findById() 这个方法按照其名称所示,根据给定的 ID 在 Repository 中查找实体: @Override Optional<T> findById(ID id); 该方法返回一个 Optional。因此,如果传递了一个不存在的 ID,返回的 Optional 对象将为 empty。 该方法使用了急切加载功能,因此只要调用该方法,就会向数据库发送请求。 执行如下示例: public User findUser(long id) { log.info("Before requesting a user in a findUser method"); Optional<User> optionalUser = repository.findById(id); log.info("After requesting a user in a findUser method"); User user = optionalUser.orElse(null); log.info("After unwrapping an optional in a findUser method"); return user; } 输出的日志如下:

Hibernate 异常 QueryException: “named parameter not bound”.

1、概览 本文将带你了解如何解决 Hibernate 异常 QueryException: “named parameter not bound”。 2、理解异常 简而言之,在将 Hibernate 查询转换为 SQL 时,由于语法无效,Hibernate 会抛出 QueryException 来提示错误。“named parameter not bound” 表明 Hibernate 无法绑定特定查询中指定的命名参数。 通常情况下,命名参数的前缀是冒号(:),后面是一个占位符,表示在执行查询之前需要设置的实际值: SELECT p FROM Person p WHERE p.firstName = :firstName; 造成异常的最常见原因之一是忘记为 Hibernate 查询中的命名参数赋值。 3、重现异常 理解了导致异常的原因后,通过一个实际的例子来重现这个异常。 创建如下 Person 实体类: @Entity public class Person { @Id private int id; private String firstName; private String lastName; // 标准的 Get、Set } 如上,@Entity 注解表示类是一个实体,它映射了数据库中的一个表。此外,@Id 表示 id 属性代表主键。 现在,创建一个带有命名参数的 Hibernate 查询,并假装忘记为参数设置值: @Test void whenNotSettingValueToNamedParameter_thenThrowQueryException() { Exception exception = assertThrows(QueryException.

将数据发送到 Kafka 中的特定分区

1、简介 Apache Kafka 是一个分布式流平台,擅长处理海量实时数据流。Kafka 将数据组织成 Topic(主题),并进一步将 Topic 划分为 Partition(分区)。每个分区都是一个独立的 Channel(通道),可实现并行处理和容错。 本文将带你了解如何把数据发送到 Kafka 中特定的分区。 2、理解 Kafka 分区 首先来了解一下 Kafka 分区的基本概念。 2.1、什么是 Kafka 分区? 当生产者向 Kafka Topic 发送消息时,Kafka 会使用指定的分区策略将这些消息组织到分区中。分区是一个基本单元,代表了线性、有序的消息序列。消息一旦产生,就会根据所选的分区策略被分配到一个特定的分区。随后,消息会被附加到该分区中日志的末尾。 2.2、并行消费与消费组 一个 Kafka Topic 可分为多个分区,一个消费组(Consumer Group)可被分配到这些分区的一个子集。组内的每个消费者都会独立处理来自其分配分区的消息。这种并行处理机制提高了整体吞吐量和可扩展性,使 Kafka 能够高效地处理大量数据。 2.3、顺序保证 在单个分区中,Kafka 可确保按照接收到的相同顺序处理消息。这保证了依赖消息顺序的应用(如金融交易或事件日志)的顺序处理。不过,需要注意的是,由于网络延迟和其他操作因素,接收消息的顺序可能与最初发送消息的顺序不同。 在不同的分区中,Kafka 并不保证顺序。来自不同分区的消息可能会被并发处理,从而带来事件顺序变化的可能性。在设计依赖于严格消息顺序的应用时,需要考虑到这个特性。 2.4、容错和高可用 分区还有助于 Kafka 实现出色的容错能力。每个分区都可以在多个 Broker 之间复制。如果 Broker 发生故障,副本分区仍可被访问,并确保对数据的持续访问。 Kafka 集群可以将消费者无缝重定向到健康的 Broker,从而保持数据的可用性和系统的高可靠性。 3、为什么要将数据发送到特定分区? 3.1、数据亲和性 数据亲和性是指有意将相关数据归入同一分区。通过将相关数据发送到特定分区,可以确保这些数据一起处理,从而提高处理效率。 例如,考虑一个场景,我们可能希望确保客户的订单位于同一个分区中,以便进行订单追踪和分析。保证特定客户的所有订单都进入同一个分区可以简化追踪和分析过程。 3.2、负载均衡 此外,在分区之间均匀地分配数据有助于确保最佳的资源利用率。在分区之间平均分配数据有助于优化 Kafka 集群内的资源利用率。通过根据负载情况向分区发送数据,可以防止出现资源瓶颈,确保每个分区都能接收到可管理的均衡工作量。 3.3、优先顺序 在某些情况下,并非所有数据都具有相同的优先级或紧迫性。Kafka 的分区功能可将关键数据引导到专用分区进行快速处理,从而实现关键数据的优先级排序。与不太重要的数据相比,这种优先级排序可确保高优先级的消息得到及时关注和更快处理。 4、向特定分区发送数据的方式 Kafka 提供了将消息分配到分区的各种策略,从而提供了数据分布和处理的灵活性。下面是一些可用于将消息发送到特定分区的常用方法。 4.1、粘性分区器(Sticky Partitioner) 在 Kafka 2.4 及以上版本中,粘性分区器(Sticky Partitioner)的目的是将没有 Key 的消息保持在同一个分区中。不过,这种行为并不是绝对的,它会与批处理设置(如 batch.

Spring 事务最佳实践

概览 本文将带你了解各种 Spring 事务的最佳实践,以保证底层业务的数据完整性。 数据完整性至关重要。如果没有适当的事务处理,应用就很容易出现竞赛条件,从而给底层业务带来可怕的后果。 模拟竞赛条件 以一个实际问题为例,说明在构建基于 Spring 的应用时,应该如何处理事务。 使用以下 Service 层和 Dao 层组件来实现转账服务: 使用最简单的 Dao 层实现来说明不按业务要求处理事务会发生什么情况: @Repository @Transactional(readOnly = true) public interface AccountRepository extends JpaRepository<Account, Long> { @Query(value = """ SELECT balance FROM account WHERE iban = :iban """, nativeQuery = true) long getBalance(@Param("iban") String iban); @Query(value = """ UPDATE account SET balance = balance + :cents WHERE iban = :iban """, nativeQuery = true) @Modifying @Transactional int addBalance(@Param("iban") String iban, @Param("cents") long cents); } getBalance 和 addBalance 方法都使用 Spring 的 @Query 注解来定义原生 SQL 查询,以检索或者修改用户的账户余额。

配置 Spring 以接收和返回 XML 格式的数据

1、概览 虽然 JSON 是 RESTful 服务的事实标准,但在某些情况下,可能需要使用 XML。例如:老掉牙的银行 API 就是通过 XML 进行交互的。 Spring 通过 Jackson XML 提供了一种简单的方法来支持 XML 端点。 2、依赖 第一步是添加 依赖。注意 spring-boot-starter-web Starter 默认不包含支持 XML 的库。需要手动添加: <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency> 另外,也可以使用 JAXB 来实现,但总的来说,JAXB 更啰嗦,而且 API 没有 Jackson 那么优雅好用。不过,如果使用的是 Java 8,JAXB 库与实现都位于 javax 包中,因此无需在应用中添加任何其他依赖。 在 Java 9 开始的版本中,javax 包被移动并更名为 jakarta,因此 JAXB 需要额外的 依赖: <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>4.0.0</version> </dependency> 另外,它需要一个运行时实现来处理 XML Mapper,这可能会导致其他的问题。 3、端点 由于 JSON 是 Spring REST Controller 的默认格式,因此需要在端点上明确配置 “消费” 和 “生产” 的数据类型是 XML:

Spring Data JPA Repository 返回 Map

1、概览 Spring JPA 为与数据库交互提供了非常灵活方便的 API。而且,还可以对其进行定制,以返回其他数据结构类型的返回值。 使用 Map 作为 JPA Repository 方法的返回类型有助于在服务和数据库之间创建更直接的交互。本文将带你了解如何在 Spring Data JPA Repository 接口的方法中返回 Map。 2、手动实现 当框架不提供某些功能时,最明显的解决方法就是自己实现。 2.1、List 可以把返回的 List 映射为 Map。通过 Stream API 只用一行代码就能实现: default Map<Long, User> findAllAsMapUsingCollection() { return findAll().stream() .collect(Collectors.toMap(User::getId, Function.identity())); } 2.2、Stream Repository 接口方法可以直接返回 Stream: @Query("select u from User u") Stream<User> findAllAsStream(); 之后,实现一个自定义方法,将结果映射到需要的数据结构中: @Transactional default Map<Long, User> findAllAsMapUsingStream() { return findAllAsStream() .collect(Collectors.toMap(User::getId, Function.identity())); } 返回 Stream 的 Repository 方法应在事务中调用。所以,直接在 default 方法中添加 @Transactional 注解。 2.3、Streamable 这与之前的方法类似。唯一的变化是返回 Streamable。先定义一个返回 Streamable 的方法:

Spring Security 配置 Basic Authentication

1、概览 本文将带你了解如何通过 Spring Security 提供的 Basic Authentication 机制来保护 MVC 应用。 2、Spring Security 配置 使用 Java 配置来配置 Spring Security: @Configuration @EnableWebSecurity public class CustomWebSecurityConfigurerAdapter { @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user1") .password(passwordEncoder().encode("user1Pass")) .authorities("ROLE_USER"); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(expressionInterceptUrlRegistry -> expressionInterceptUrlRegistry.requestMatchers("/securityNone").permitAll() .anyRequest().authenticated()) .httpBasic(httpSecurityHttpBasicConfigurer -> httpSecurityHttpBasicConfigurer.authenticationEntryPoint(authenticationEntryPoint)); http.addFilterAfter(new CustomFilter(), BasicAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } 如上,在 SecurityFilterChain Bean 中使用 httpBasic() 来定义 Basic Authentication。

Spring Security 设置 Authentication Provider

1、概览 本文将带你了解如何在 Spring Security 中设置 Authentication Provider,相比于使用简单的 UserDetailsService 的标准方案,这样可以提供额外的灵活性。 2、Authentication Provider Spring Security 提供了多种执行身份认证的选项。这些选项遵循一个简单的契约;一个 AuthenticationProvider 处理一个身份认证请求,并返回一个带有完整凭证的认证对象。 最常见的标准实现是 DaoAuthenticationProvider,它从一个简单的 UserDetailsService(只读)中获取用户详细信息。这个 Service 只能通过用户名检索完整的用户实体,这在大多数情况下已足够。 更多的自定义场景仍需要访问完整的认证请求才能执行身份验证流程。例如,在对某些外部第三方服务进行身份验证时,身份认证请求中的 username 和 password 都是必要的。 对于这些更高级的场景,需要定义一个自定义的 Authentication Provider: @Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String name = authentication.getName(); String password = authentication.getCredentials().toString(); if (shouldAuthenticateAgainstThirdPartySystem()) { // 使用凭证并对第三方系统进行身份认证 return new UsernamePasswordAuthenticationToken( name, password, new ArrayList<>()); } else { return null; } } @Override public boolean supports(Class<?

Spring Security 控制 Session

1、概览 本文将带你了解如何在 Spring Security 中配置 Session 超时、Session 并发以及其他高级的 Session 安全设置。 2、何时创建 Session? 可以精确控制 Session 的创建时间,以及 Spring Security 与 Session 的交互方式: always:如果 Session 不存在,则会创建一个 Session。 ifRequired:仅在需要时才创建 Session(默认值)。 never:框架不会自己创建 Session,但如果 Session 已经存在,则会使用该 Session。 stateless:Spring Security 不会创建或使用 Session。 <http create-session="ifRequired">...</http> Java 配置如下: @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) return http.build(); } 注意,该配置只控制 Spring Security 的行为,而不是整个应用。如果指定 Spring Security 不创建 Session,它就不会创建 Session,但是应用可能会创建 Session! 默认情况下,Spring Security 会在需要时创建一个 Session,即 ifRequired。 对于无状态应用,never 选项将确保 Spring Security 本身不会创建任何 Session。但如果应用创建了 Session,Spring Security 就会使用它。

在 Spring Boot 中记录完整的请求体和响应体日志

完整的请求日志对于 故障排查 和 审计 来说极其重要。通过查看日志,可以检查数据的准确性、参数的传递方式以及服务器返回的数据。 由于 Socket 流不能重读,所以需要一种实现来把读取和写入的数据缓存起来,并且可以多次重复读取缓存的内容。 Spring 提供 2 个可重复读取请求、响应的 Wrapper 工具类: ContentCachingRequestWrapper ContentCachingResponseWrapper 通过类名不难看出,这是典型的装饰者设计模式。它俩的作用就是把读取到的 请求体 和写出的 响应体 都缓存起来,并且提供了访问缓存数据的 API。 创建 RequestLogFilter 创建 RequestLogFilter 继承 HttpFilter,以记录完整的请求和响应日志。 package cn.springdoc.demo.web.filter; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * 记录请求日志 */ public class RequestLogFilter extends HttpFilter { static final Logger log = LoggerFactory.getLogger(RequestLogFilter.class); /** * */ private static final long serialVersionUID = 8991118181953196532L; @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // Wrapper 封装 Request 和 Response ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response); // 继续执行请求链 chain.