1.请求和响应中多次获取流中数据异常处理
-
SpringMVC请求流中数据只能被使用一次,如果多次使用就会产生异常。
- 如果使用了Post请求传送数据,在DispatcherServlet中doDispatch()中会将数据转换为controller中@RequestBody注解需要的数据,此时使用HttpServletRequest.getInputStream()获取数据,
- 在:切面、拦截器、过滤器调用了HttpServletRequest.getReader();获取数据时会报错,因为已经使用了HttpServletRequest.getInputStream()获取。
- 处理方式:使用Filter+包装请求。Filter中根据路径判断是否需要包装请求。
@Component public class RequestAndResponseConvertFilter implements Filter { private final Set<String> needHandlerRequestPath = Set.of("/test02"); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if(request instanceof HttpServletRequest servletRequest) { String requestURI = servletRequest.getRequestURI(); if (needHandlerRequestPath.contains(requestURI)){ chain.doFilter(new BodyHttpServletRequestWrapper(servletRequest), response); } else { chain.doFilter(request, response); } } } } public class BodyHttpServletRequestWrapper extends HttpServletRequestWrapper { // 包装之后,请求中每次获取数据就获取body中的数据。 private final byte[] body; public BodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); body = IoUtil.readBytes(request.getInputStream()); } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() { final ByteArrayInputStream in = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener listener) { } @Override public int read() { return in.read(); } }; } @Override public String getHeader(String name) { return super.getHeader(name); } @Override public Enumeration<String> getHeaderNames() { return super.getHeaderNames(); } @Override public Enumeration<String> getHeaders(String name) { return super.getHeaders(name); } }
2.问题和解决方案-@RestControllerAdvice注解无法处理404
- 原因。
- SpringMVC会为每个controller中的请求方法创建一个Handler,然后通过这个Handler来建立请求URL和controller方法的关系。
- 404,是没有找到Handler,没有进入到方法中,而@RestControllerAdvice捕获的异常是对controller中的每个方法建立了切面,没有走到方法中所以无法处理。
- 解决方式一,yaml中添加配置。(测试环境使用了swagger,则无法使用该解决方式)
spring:
web:
# false,不会将静态资源的URL路径添加到映射中,可以减少减少Spring的URL路径映射的负担。
# false,生产环境可以设置为false。
# 如果测试环境中使用了swagger文档,则需要设置为true,需要为swagger需要的静态资源建立映射。
resources:
add-mappings: false
mvc: # 让DispatcherServlet向上抛出异常,这样@RestControllerAdvice就可以捕捉到。
throw-exception-if-no-handler-found: true
- 解决方式二,yaml+代码。(和解决方式二的原来一样,让DispatcherServlet向上抛出异常,也无法处理需要swagger的情况)
spring:
web:
resources:
add-mappings: false
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DispatcherServlet dispatcherServlet) {
// 通过代码让DispatcherServlet向上抛出异常
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
}
return bean;
}
}
- 解决方式三,使用HandlerInterceptor来处理404。添加一个拦截器,当响应时404时直接,返回对应的Json数据。
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
private final static String RESPONSE_404_JSON_DATA = "{\"code\":\"404\",\"success\":false,\"data\":null,\"message\":\"请求资源不存在\"}";
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (response.getStatus() == HttpServletResponse.SC_NOT_FOUND) {
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.getWriter().print(RESPONSE_404_JSON_DATA);
return false;
}
return true;
}
}).addPathPatterns("/**");
}
}
- 解决方式四,继承BasicErrorController来处理404。SpringMVC有两种异常处理:BasicErrorController和@ControllerAdvice。BasicErrorController处理非Controller抛出的异常(即还没有进去controller抛出的异常),而@ControllerAdvice用于处理Controller抛出的异常,对于非Controller抛出的异常它是不会管的(因为其基于切面)。
@Slf4j
@RestController
@RequestMapping("${server.error.path:${error.path:/error}}")
public class ErrorController extends BasicErrorController {
public ErrorController(ServerProperties serverProperties) {
super(new DefaultErrorAttributes(), serverProperties.getError());
}
@Override
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
Map<String, Object> map = new HashMap<>();
map.put("code", 404);
map.put("success", false);
map.put("data", null);
map.put("message", "请求资源不存在");
return map;
}
}
3.问题和解决方案-SpringMVC处理404的过程
- 请求进来,先去找处理当前请求的HandlerMethod,即controler中对应的方式。
- 没有匹配到HandlerMethod,然后将当前资源当做web资源,使用ResourceHttpRequestHandler来在
[classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
下查找对应的资源。(如果配置了spring.web.resources.add-mappings: false
,就不会为web资源建立url映射,即不会在进行web资源的查找。)。 - 如果没有找到,就去使用BasicErrorController的处理器来处理404。
4.如何在切面中获取响应数据
5.SpringMVC映射文件系统中的静态资源,映射之后可以通过浏览器直接访问静态资源
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 浏览器访问http://localhost:9001/upload/1.jpg,会进行映射。
// 实际访问的是文件系统中的D:/Temp/upload/1.jpg。
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:D:/Temp/upload/");
}
}
6.SpringMVC中添加controller方法中参数的解析
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
log.info("参数解析器 {}", resolvers);
resolvers.add(new CustomerHandlerMethodArgumentRevolver());
}
}
public class CustomerHandlerMethodArgumentRevolver implements HandlerMethodArgumentResolver {
/**
* 当请求的方法参数是UserForm时,使用该参数解析器。
* @param parameter the method parameter to check
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(UserForm.class);
}
/**
* 返回创建的UserForm对象。
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
UserForm form = new UserForm();
form.setId(IdUtil.getSnowflakeNextId());
form.setName("tom");
return form;
}
}
7.Json序列化时将long转换为String类型
-
JS中Number的精度为16位(最大位17位,第17位精度不准)。Java的long为18位,传到客户端会丢失最后两位。
-
解决办法,在Json序列化时将long类型转换为String类型。
- 使用注解将long转换为String,缺点每个字段都需要添加。
@JsonSerialize(using = ToStringSerializer.class) private Long id;
- 配置通用类型转换,缺点,所有的long类型都会转换为String类型。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Bean @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); SimpleModule simpleModule = new SimpleModule(); // BigInteger,大整形,转换为String类型。 simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); // Long转换为String类型。 simpleModule.addSerializer(Long.class, ToStringSerializer.instance); // long基本类型转换为String类型。 simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(simpleModule); return objectMapper; } }
8.枚举类型和json的转换
// 枚举类型。
@Getter
@AllArgsConstructor
public enum UserTypeEnum {
ROOT(1, "root"),
ADMIN(2, "admin"),;
private final Integer type;
private final String desc;
}
// 实体类,包含枚举类型的属性。
@Data
public class UserVO {
private Long id;
private UserTypeEnum userTypeEnum;
}
-
枚举转换为json字符串。(使用@JsonValue注解修改json的序列化结果)
- controller层的返回参数为UserVO,默认情况下,返回的json数据如下。
{ "id": 123, "userTypeEnum": "ROOT" }
- 如果想输出的json为UserTypeEnum的desc字段,则可以使用@JsonValue字段。使用了@JsonValue注解之后,返回的json为
{..., "userTypeEnum": "root"}
public enum UserTypeEnum { ROOT(1, "root"), ADMIN(2, "admin"), ; @Getter private final Integer type; private final String desc; UserTypeEnum(Integer type, String desc) { this.type = type; this.desc = desc; } @JsonValue public String getDesc() { return desc; } }
-
将字符传转换为枚举类型。(json反序列化时枚举类型的处理方式)
@PostMapping("/test01") public UserVO test01(@RequestBody UserVO vo) { return vo; }
- 默认情况下,映射UserVO中的UserTypeEnum枚举类型属性需要传入字符串"ROOT"或者"ADMIN"。(传入的json数据为
{..., userTypeEnum: "ROOT"}
) - 使用@JsonValue完成json的反序列化,需要映射UserTypeEnum就需要传"root"或者"admin"。(传入的json数据为
{..., userTypeEnum: "root"}
)
public enum UserTypeEnum { ROOT(1, "root"), ADMIN(2, "admin"), ; @Getter private final Integer type; private final String desc; UserTypeEnum(Integer type, String desc) { this.type = type; this.desc = desc; } @JsonValue public String getDesc() { return desc; } }
- 使用@JsonCreator完成json的反序列化,如果@JsonValue和@JsonCreator同时存在,则使用@JsonCreator完成json反序列化。(传入的json数据为
{..., userTypeEnum: "1"}
)
public enum UserTypeEnum { ROOT(1, "root"), ADMIN(2, "admin"), ; @Getter private final Integer type; private final String desc; UserTypeEnum(Integer type, String desc) { this.type = type; this.desc = desc; } @JsonValue public String getDesc() { return desc; } @JsonCreator public static UserTypeEnum getByType(Integer type) { for (UserTypeEnum value : values()) { if (Objects.equals(type, value.type)) { return value; } } return null; } }
- 默认情况下,映射UserVO中的UserTypeEnum枚举类型属性需要传入字符串"ROOT"或者"ADMIN"。(传入的json数据为
-
SpringMVC框架中将字符串转换为枚举类型。
- 请求路径/test01?userTypeEnum=ROOT,这种方式的传参不涉及json的反序列化,所以@JsonValue和@JsonCreator注解此时也是无效的,并且此时需要传入字符串ROOT或者ADMIN才能被识别。
@GetMapping("/test01") public UserVO test01(UserTypeEnum userTypeEnum) { return new UserVO(); }
- 通过WebMvcConfigurer.addFormatters()修改SpringMVC默认映射规则。修改之后的规则为枚举中有添加了@JsonCreaor、@JsonValue的注解,则使用该注解映射枚举,没有该注解,则使用默认的映射(即需要传入ROOT或者ADMIN)。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter()); } } // 将前端传入的字符串转换为枚举类型。 @Slf4j public class StringToEnumConverter implements ConditionalGenericConverter { private final Object OBJ = new Object(); private final Map<Class<?>, Object> CACHE_ENUM_METHOD = new ConcurrentHashMap<>(); /** * 之后目标类型是枚举才进行处理。 * @param sourceType the type descriptor of the field we are converting from * @param targetType the type descriptor of the field we are converting to * @return */ @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return targetType.getObjectType().getSuperclass().equals(Enum.class); } @Override public Set<ConvertiblePair> getConvertibleTypes() { return Set.of(new ConvertiblePair(String.class, Enum.class)); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (!sourceType.getObjectType().equals(String.class)) { return null; } String str = (String) source; if (StrUtil.isBlank(str)) { return null; } Class<?> objectType = targetType.getObjectType(); Object method = CACHE_ENUM_METHOD.get(objectType); if (method == null) { method = getMethod(objectType); CACHE_ENUM_METHOD.put(objectType, method); } if (OBJ.equals(method)) { // 枚举上没有带@JsonValue、@JsonCreator的注解,就使用枚举默认的映射方式。 return valueOf(objectType, str); } else { Method m = (Method) method; try { // 每次都需要通过返回获取枚举的实力对象,可以优化为增加一层映射,以加快执行速度。 // 1 -> ROOT,2 -> ADMIN。 return m.invoke(objectType, source); } catch (IllegalAccessException | InvocationTargetException e) { log.error("枚举类型处理异常", e); throw new RuntimeException(e); } } } @SuppressWarnings("unchecked") private <T extends Enum<T>> T valueOf(Class<?> clazz, String value){ return Enum.valueOf((Class<T>) clazz, value); } private Object getMethod(Class<?> clazz) { Method[] declaredMethods = clazz.getDeclaredMethods(); // 找带有@JsonCreator注解的方法。 for (Method method : declaredMethods) { JsonCreator annotation = method.getAnnotation(JsonCreator.class); if (annotation != null && JsonCreator.Mode.DISABLED != annotation.mode()) { method.setAccessible(true); return method; } } // 找带有@JsonValue注解的方法。 for (Method method : declaredMethods) { JsonValue annotation = method.getAnnotation(JsonValue.class); if (annotation != null) { method.setAccessible(true); return method; } } return OBJ; } }