配置 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.

在 Spring Security 中获取当前用户

1、概览 本文介绍了如何在 Spring Security 中检索当前用户详细信息。 2、从 Bean 中获取用户 检索当前已通过身份认证用户(Principal)的最简单方式是调用 SecurityContextHolder 的静态方法: Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String currentPrincipalName = authentication.getName(); 该代码需要改进的地方在于,在尝试访问之前,首先要检查是否存在已通过身份认证的用户: Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!(authentication instanceof AnonymousAuthenticationToken)) { String currentUserName = authentication.getName(); return currentUserName; } 当然,这样的静态调用也有缺点,代码的可测试性降低了。 3、在 Controller 中获取用户 在 @Controller 中,可以直接将 Principal 定义为方法参数,这样框架就能正确地解析: @Controller public class GetUserWithPrincipalController { @RequestMapping(value = "/username", method = RequestMethod.GET) @ResponseBody public String currentUserName(Principal principal) { return principal.getName(); } } 或者,也可以使用 Authentication Token: @Controller public class GetUserWithAuthenticationController { @RequestMapping(value = "/username", method = RequestMethod.

配置 Apache HttpClient 信任所有 SSL 证书

1、概览 本文将带你了解如何配置 Apache HttpClient 4 和 5 以支持 “Accept All”(接受所有)SSL。目标很简单 - 信任所有证书,包括无效的 SSL 证书。 2、SSLPeerUnverifiedException 如果不在 HttpClient 上配置 SSL,下面的测试(使用 HTTPS URL)就会失败: @Test void whenHttpsUrlIsConsumed_thenException() { String urlOverHttps = "https://localhost:8082/httpclient-simple"; HttpGet getMethod = new HttpGet(urlOverHttps); assertThrows(SSLPeerUnverifiedException.class, () -> { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpResponse response = httpClient.execute(getMethod, new CustomHttpClientResponseHandler()); assertThat(response.getCode(), equalTo(200)); }); } 确切的异常是: javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397) at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:126) ... 如果无法为 URL 建立有效的信任链(Trust Chain),就会出现 javax.net.ssl.SSLPeerUnverifiedException 异常。 3、配置 SSL - Accept All(HttpClient 5) 现在,配置 HTTP 客户端信任所有证书链,无论其是否有效:

在 Spring 应用中为 REST API 实现异常处理

1、概览 本文将地带你了解如何在 Spring 中为 REST API 实现异常处理。 在 Spring 3.2 之前,在 Spring MVC 应用中处理异常的两种主要方法是 HandlerExceptionResolver 或 @ExceptionHandler 注解。这两种方法都有一些明显的缺点。 自 3.2 以来,可以使用 @ControllerAdvice 注解来解决前两种解决方案的局限性,并促进整个应用中统一的异常处理。 Spring 5 引入了 ResponseStatusException 类,一种在 REST API 中进行基本错误处理的快速方法。 所有这些都有一个共同点:它们都很好地处理了关注点的分离。应用通常可以抛出异常来表示某种失败,然后再单独进行处理。 2、解决方案 1:Controller 级的 @ExceptionHandler 第一种解决方案适用于 @Controller 层面。定义一个处理异常的方法,并用 @ExceptionHandler 进行注解: public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } } 这种方法有一个很大的缺点:@ExceptionHandler 注解方法仅对特定 Controller 有效,而不是对整个应用全局有效。当然,可以将其添加到每个 Controller 中,但这并不适合作为通用的异常处理机制。 也可以通过让所有 Controller 都继承一个 Base Controller 类来绕过这一限制。 然而,对于某些原因无法实现上述方法的应用来说,这种解决方案可能会成为一个问题。例如,Controller 可能已经从另一个 Base 类继承而来,而该 Base 类可能在另一个 Jar 中或不可直接修改,或者 Controller 本身不可直接修改。

Spring 中的 @Scheduled 注解

1、概览 本文将带你了解如何使用 Spring @Scheduled 注解来配置和调度定时任务。 使用 @Scheduled 对方法进行注解时,需要遵循如下简单的规则: 方法的返回类型通常应为 void(如果不是,返回值将被忽略) 方法不应有任何参数 2、启用定时调度 可以在配置类上使用 @EnableScheduling 注解来启用 Spring 中的定时任务和 @Scheduled 注解的支持: @Configuration @EnableScheduling public class SpringConfig { ... } 也可以在 XML 中启用,如下: <task:annotation-driven> 3、以固定延迟调度任务 配置一个任务,使其在固定延迟后运行: @Scheduled(fixedDelay = 1000) public void scheduleFixedDelayTask() { System.out.println( "Fixed delay task - " + System.currentTimeMillis() / 1000); } 如上,上一次执行结束与下一次执行开始之间的持续时间是固定的。任务会一直等待到前一个任务结束。 在必须确保上一次执行完成后再次运行的情况下,应使用此选项。 4、以固定频率调度任务 在固定的时间间隔内执行一项任务: @Scheduled(fixedRate = 1000) public void scheduleFixedRateTask() { System.out.println( "Fixed rate task - " + System.currentTimeMillis() / 1000); } 如果任务的每次执行都是独立的,则应使用该选项。