首页 > 其他分享 >SpringMVC进阶-02

SpringMVC进阶-02

时间:2024-05-25 10:18:10浏览次数:19  
标签:02 return String SpringMVC private class Override public 进阶

1.请求和响应中多次获取流中数据异常处理

  1. SpringMVC请求流中数据只能被使用一次,如果多次使用就会产生异常。

    1. 如果使用了Post请求传送数据,在DispatcherServlet中doDispatch()中会将数据转换为controller中@RequestBody注解需要的数据,此时使用HttpServletRequest.getInputStream()获取数据,
    2. 在:切面、拦截器、过滤器调用了HttpServletRequest.getReader();获取数据时会报错,因为已经使用了HttpServletRequest.getInputStream()获取。
    3. 处理方式:使用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

  1. 原因。
    1. SpringMVC会为每个controller中的请求方法创建一个Handler,然后通过这个Handler来建立请求URL和controller方法的关系。
    2. 404,是没有找到Handler,没有进入到方法中,而@RestControllerAdvice捕获的异常是对controller中的每个方法建立了切面,没有走到方法中所以无法处理。
  2. 解决方式一,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
  1. 解决方式二,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;
    }
}
  1. 解决方式三,使用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("/**");
    }
}
  1. 解决方式四,继承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的过程

  1. 请求进来,先去找处理当前请求的HandlerMethod,即controler中对应的方式。
  2. 没有匹配到HandlerMethod,然后将当前资源当做web资源,使用ResourceHttpRequestHandler来在[classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]下查找对应的资源。(如果配置了spring.web.resources.add-mappings: false,就不会为web资源建立url映射,即不会在进行web资源的查找。)。
  3. 如果没有找到,就去使用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类型

  1. JS中Number的精度为16位(最大位17位,第17位精度不准)。Java的long为18位,传到客户端会丢失最后两位。

  2. 解决办法,在Json序列化时将long类型转换为String类型。

    1. 使用注解将long转换为String,缺点每个字段都需要添加。
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    
    1. 配置通用类型转换,缺点,所有的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;
}
  1. 枚举转换为json字符串。(使用@JsonValue注解修改json的序列化结果)

    1. controller层的返回参数为UserVO,默认情况下,返回的json数据如下。
    {
        "id": 123,
        "userTypeEnum": "ROOT"
    }
    
    1. 如果想输出的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;
        }
    }
    
  2. 将字符传转换为枚举类型。(json反序列化时枚举类型的处理方式)

    @PostMapping("/test01")
    public UserVO test01(@RequestBody UserVO vo) {
        return vo;
    }
    
    1. 默认情况下,映射UserVO中的UserTypeEnum枚举类型属性需要传入字符串"ROOT"或者"ADMIN"。(传入的json数据为{..., userTypeEnum: "ROOT"}
    2. 使用@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;
        }
    }
    
    1. 使用@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;
        }
    }
    
  3. SpringMVC框架中将字符串转换为枚举类型。

    1. 请求路径/test01?userTypeEnum=ROOT,这种方式的传参不涉及json的反序列化,所以@JsonValue和@JsonCreator注解此时也是无效的,并且此时需要传入字符串ROOT或者ADMIN才能被识别。
    @GetMapping("/test01")
    public UserVO test01(UserTypeEnum userTypeEnum) {
        return new UserVO();
    }
    
    1. 通过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;
        }
    }
    

标签:02,return,String,SpringMVC,private,class,Override,public,进阶
From: https://www.cnblogs.com/godistance/p/18212119

相关文章