背景
将HTTP请求地址重定向到请求体内传入的请求接口名。
相关依赖
完成拦截器用到的两个注解:@WebFilter、@ServletComponentScan,分别在 Tomcat 依赖包和 Springboot 依赖包中。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.1</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>10.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
relativePath : 查找顺序。指定获取路径,设定一个空值将始终从仓库中获取,不从本地路径获取。
spring-boot-starter-web 的依赖中已经包含了 Tomcat 依赖,直接引入 Springboot 的包即可。
实现
使用拦截器的方式实现重定向
首先因为重定向的地址存在于请求的body中,而HttpServletRequest只能读取一次body流,所以需要实现一个RequestWrapper,来保存从原来请求中读取出来的body,并且需要修改相应的获取参数和body流的方法,实现参数的多次读取。
package com.test;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.io.IOUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.*;
@Getter
@Setter
public class RequestWrapper extends HttpServletRequestWrapper {
/**
* 请求接口名参数 key
*/
private static final String COMMAND_ID = "command_id";
/**
* URL分隔符
*/
private static final String URL_SEPARATOR = "/";
/**
* 请求体
*/
private String body;
/**
* 请求参数
*/
private Map<String, String[]> paramMap = new HashMap<>();
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = analysisBodyString(request);
this.paramMap.putAll(request.getParameterMap());
}
/**
* 从请求体中解析请求地址
*
* @return 请求地址
*/
public String getRequestUrl() {
return URL_SEPARATOR + JSON.parseObject(body).getString(COMMAND_ID);
}
@Override
public String getParameter(String name) {
String[] values = paramMap.get(name);
return values == null || values.length < 1 ? null : values[0];
}
@Override
public Map<String, String[]> getParameterMap() {
return paramMap;
}
@Override
public Enumeration<String> getParameterNames() {
if (paramMap.isEmpty()) {
return super.getParameterNames();
}
Set<String> paramNames = new LinkedHashSet<>();
paramNames.addAll(Collections.list(super.getParameterNames()));
paramNames.addAll(paramMap.keySet());
return Collections.enumeration(paramNames);
}
@Override
public String[] getParameterValues(String name) {
return paramMap.get(name);
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteInputStream = new ByteArrayInputStream(body.getBytes());
// 参考 ServletInputStream 内部实现 BodyInputStream 类的方法定义
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return byteInputStream.read();
}
@Override
public void close() throws IOException {
byteInputStream.close();
}
@Override
public synchronized void reset() throws IOException {
byteInputStream.reset();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
/**
* 解析请求参数
*
* @param request servletRequest
* @return 请求体-String
* @throws IOException
*/
private String analysisBodyString(final HttpServletRequest request) throws IOException {
String contentType = request.getContentType();
String bodyString = "";
StringBuilder sb = new StringBuilder();
// 表单类型请求特殊处理
boolean isForm = !contentType.isBlank() && (contentType.contains("multipart/form-data") || contentType.contains("x-www-form-urlencoded"));
if (isForm) {
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> next : parameterMap.entrySet()) {
String[] values = next.getValue();
String value = null;
if (values != null) {
if (values.length == 1) {
value = values[0];
} else {
value = Arrays.toString(values);
}
}
sb.append(next.getKey()).append("=").append(value).append("&");
}
if (!sb.isEmpty()) {
bodyString = sb.substring(0, sb.toString().length() - 1);
}
return bodyString;
} else {
// 使用系统默认字符集将请求输入转换成字符串
return IOUtils.toString(request.getInputStream(), Charset.defaultCharset());
}
}
}
在完成了RequestWapper的编写后,定义一个拦截器,将原有的HttpRequest请求转换为RequestWrapper,进行参数的读取。
package com.test;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/test")
@Order(1)
public class FrontRequestCommandFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
RequestDispatcher requestDispatcher = request.getRequestDispatcher(requestWrapper.getRequestUrl());
requestDispatcher.forward(requestWrapper, response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
因为使用了@WebFilter注解,还需要在Springboot启动类上面加上@ServletComponentScan注解以开启功能。
package com.test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
public class BootstrapApplication {
public static void main(String[] args) {
SpringApplication.run(BootstrapApplication.class, args);
}
}
注意事项
1、在进行消息重定向时,不能传递原来的HttpRequest,因为在解析 body 的时候已经读取了请求的输入流。HttpRequest 的body输入流只能读取一次,后续controller层使用 @RequestBody 会读取不到数据。所以直接传递包装之后的 RequestWrapper,Wrapper 中已经重写了 getInputStream 方法,能够实现 @RequestBody 注解功能。
2、在匹配HTTP请求的媒体类型时,不同的浏览器可能会有不同的值,所以不能简单的使用字符串匹配,应使用 MediaType 提供的类型转换方法: MediaType requestType = MediaType.parseMediaType(contentType)。MediaType 重写了 equals 方法,可以直接使用 equals 比较两个媒体类型是否一致。
使用 ModelAndView 实现重定向
在 controller 中直接返回携带有视图名称的 ModelAndView 实现重定向
@RequestMapping(value = "/test", method = {RequestMethod.POST, RequestMethod.GET})
public ModelAndView forward(HttpServletRequest request) {
String url = request.getParameter("url");
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(url);
return modelAndView;
}
参考文献
Spring MVC页面重定向spring boot使用servletFilter实现重定向SpringBoot项目后端重定向的问题Springboot注解@ServletComponentScan和@ComponentScanSpringBoot下,利用@WebFilter配置使用与注意FilterServlet3.0下@WebFilter注解配置FilterHttpServletRequest获取POST请求Body参数3种方法