跳至主要內容

SpringMVC拦截器与数据验证

wangdx大约 34 分钟

拦截器案例实现说明

正确的请求与响应

  • 在动态 WEB 应用程序开发时,请求处理与响应结果是应用开发的核心主题,只有在客户端发送了正确的请求后,服务端才可以进行正确的业务处理,以及有效的数据响应

过滤保护

  • 所有的 WEB 应用都是运行在公网上,并且可以使用的资源路径也都暴露在外部,这样用户在进行请求访问的时候,如果没有按照 WEB 应用设计的标准进行数据的传输,那么最终就会导致数据转换处理产生异常,或者更严重的情况下,传输了一些恶意的数据导致项目的核心数据被泄漏。所以在这样的处理环境下就需要对应用程序进行保护,而在标准 JakartaEE 设计中可以通过过滤器来实现请求数据验证逻辑的定义

基于拦截器实现数据验证

  • 由于 JakartaEE 仅仅提供了技术标准,所以现代的 Java 项目开发中都会基于 Spring 开发框架,以方便的实现 Bean 管理与动态代理设计的支持,针对于 WEB 开发也提供了 SpringMVC 框架的支持,但是 SpringMVC 开发处理之中并不建议使用过滤器的方式来进行拦截,主要的原因在于:
  • SpringMVC 是基于 DispatcherServet 类实现所有请求的分发处理操作,而过滤器是与 DispatcherServet 同级别的 WEB 应用组件,所以如果直接编写过滤器进行处理,那么将无法使用 Spring 中的 Bean 管理机制,更无法准确的找到控制层路径对应的处理类的信息。所以要想在 SpringMVC 之中实现这样的请求数据验证过滤处理,就只能够通过内置拦截器组件来实现

拦截器验证处理逻辑

  • 在一个项目之中会存在有大量的控制器,所以也会提供有大量需要进行请求数据验证的访问路径存在,那么此时就需要基于可重用的设计思想,通过合理的面向对象设计,基于拦截器处理机制,实现请求数据验证

资源文件实现规则配置

  • 为了更加灵活的实现请求数据的验证处理,开发者可以通过 validation.properties 资源文件来定义指定控制层处理方法的验证规则,而验证规则是在系统中由开发者自行定义的,同时在设计时也需要保留程序的可扩展性。因为不同的应用环境会有不同的应用规则存在,所以在进行设计时就要充分的考虑到各种应用场景以及操作化性的问题
  • Spring 提供了 MessageSource 接口,可以在 Spring 容器启动时进行所有配置规则的加载,而在每次进行请求拦截时,基于 HandlerMethod 对象实例进行规则 KEY 的拼凑并通过 MessageSource 实例进行规则查找,最后使用专属的验证类进行规则的判断如果用户发送的请求数据满足验证规则的需要,则将请求转发到目标 Action,如果不满足数据验证规则,则直接跳转到错误页进行显示。

维护验证规则配置

  • 此已经得到了关于数据验证处理的初步实现方案但是在这个实现方案的背后会存在有另外一个现实的问题,那么就是代码维护。试想一下在一个完整的项目应用之中,会存在大量的 Action 程序类,每一个 Action 程序类中都可能包含有若干个控制层处理方法,而如果将每一个控制层方法的验证规则都写在一个 validation.properties 文件之中,则配置项一定会非常的多,并且在维护时也非常的繁琐,一个基础的 CRUD 控制器所应有的验证项配置。
1、
package com.yootk.action;
@Controller
public class MessageAction {
	public String add(){} // 此方法需要验证
	public String echo(){} // 此方法不需要验证
}

2、
com.yootk.action.MessageAction.add=验证规则
com.yootk.action.MessageAction.echo=验证规则

搭建案例开发环境

案例项目与子模块

  • 在项目开发中一般都会提供有一些专属的工具模块,以供不同的子模块去使用,考虑到数据验证为一个常用的功能,所以在进行项目构建时可以采用公共子模块的形式定义

项目依赖管理标准实现架构

  • 在本次的案例讲解中,所有的案例都将保存在“ssm-case”项目之中,读者可以根据子模块的名称找到对应的实现源代码,由于不同开发环境的需要,所以只会在 ssm-case 项目之中配置 Spring 的核心依赖,而关于 SpringMVC 有关的依赖配置会在不同的子模块中进行定义。
  • 如果现在开发者希望在项目中使用不同的项目进行代码的管理,那么就需要引入 Nexus 本地私而后再通过 Nexus 私服进行依赖库服进行管理,不同的项目直接打包发布到 Nexus 私服之中,在本系列的《Java 项目构建与代码管理开发实战》中已经详细讲解了此操作的实现的引入如果未掌握的读者请自行参考,也请有需要的读者自行搭建相关服务环境。
1、
project_group=com.yootk
project_version=1.0.0
project_jdk=17

2、
project(":common") { // 定义的是公共的程序类
    dependencies{
        implementation(libraries.'spring-web')
        implementation(libraries.'spring-webmvc')
        implementation(libraries.'servlet-api')
        implementation(libraries.'jackson-core')
        implementation(libraries.'jackson-databind')
        implementation(libraries.'jackson-annotations')
    }
}

3、
project(":validate-case") { // 要编写案例的项目模块
    dependencies{
        implementation(libraries.'spring-web')
        implementation(libraries.'spring-webmvc')
        implementation(libraries.'jstl-api')
        implementation(libraries.'taglibs-standard')
        implementation(libraries.'jsp-api')
        implementation(libraries.'servlet-api')
        implementation(libraries.'jackson-core')
        implementation(libraries.'jackson-databind')
        implementation(libraries.'jackson-annotations')
        implementation(project(':common')) // 引入其他模块
    }
}

4、
package com.yootk.common.web.action.abs;

import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;

import java.beans.PropertyEditorSupport;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class AbstractAction { // 定义公共的控制层父类
    private static final DateTimeFormatter LOCAT_DATE_FORMAT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 日期格式化的处理格式
    @InitBinder // 初始化绑定处理
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(java.util.Date.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                LocalDate localDate = LocalDate.parse(text, LOCAT_DATE_FORMAT);
                Instant instant = localDate.atStartOfDay()
                        .atZone(ZoneId.systemDefault()).toInstant();
                super.setValue(java.util.Date.from(instant)); // 字符串与日期的转换
            }
        });
    }
}


5、
package com.yootk.common.web.servlet;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import java.util.HashMap;
import java.util.Map;

public class YootkDispatcherServlet extends DispatcherServlet { // 自定义Servlet
    private boolean restSwitch = false; // REST响应模式开关
    // Jackson提供的数据转换的工具类,用这个类将响应的错误信息转为JSON结构
    private ObjectMapper mapper = new ObjectMapper();
    public YootkDispatcherServlet(WebApplicationContext webApplicationContext) {
        super(webApplicationContext);
    }
    public void setRestSwitch(boolean restSwitch) { // REST开关控制
        this.restSwitch = restSwitch;
    }

    @Override
    protected void noHandlerFound(HttpServletRequest request,
                                  HttpServletResponse response) throws Exception {
        // 在这个地方进行404错误的处理,SpringBoot这方面做的比较成功
        if (this.restSwitch) { // 使用REST风格进行错误的展示
            response.setStatus(HttpStatus.NOT_FOUND.value()); // 响应状态
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            Map<String, Object> result = new HashMap<>(); // 保存错误信息
            result.put("message", "请求路径未发现,无法进行请求处理!");
            result.put("status", HttpStatus.NOT_FOUND); // 设置一个状态码
            response.getWriter().print(this.mapper.writeValueAsString(result)); // JSON响应
        } else {
            response.sendRedirect("/notfound"); // 按照传统方式跳转
        }
    }
}


6、
package com.yootk.common.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration // 需要明确的设置扫描路径
public class ResourceViewConfig { // 视图资源配置类
    @Bean
    public InternalResourceViewResolver resourceViewResolver() {
        InternalResourceViewResolver resolver =
                new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/pages"); // 路径的前缀
        resolver.setSuffix(".jsp"); // 路径后缀
        return resolver;
    }
}


7、
package com.yootk.validate.context.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.yootk.common.web.config") // common模块中的类
public class SpringApplicationContextConfig {}


8、
package com.yootk.common.mapper;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import java.text.SimpleDateFormat;
import java.util.TimeZone;

public class CustomObjectMapper extends ObjectMapper {
    private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; // 日期格式
    public CustomObjectMapper() {
        super.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_FORMAT));
        super.configure(SerializationFeature.INDENT_OUTPUT, true); // 格式化输出
        super.setSerializationInclusion(JsonInclude.Include.NON_NULL); // NULL不参与配置
        super.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); // 时区
    }
}


9、
package com.yootk.validate.context.config;

import com.yootk.common.mapper.CustomObjectMapper;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@EnableWebMvc // 启用SpringMVC
@ComponentScan("com.yootk.validate.action") // 扫描控制层路径
public class SpringWebContextConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 所有的资源一定要保存在WEB-INF目录之中,那么就必须进行资源映射
        registry.addResourceHandler("/yootk-js/**")
                .addResourceLocations("/WEB-INF/static/js/");
        registry.addResourceHandler("/yootk-css/**")
                .addResourceLocations("/WEB-INF/static/css/");
        registry.addResourceHandler("/yootk-images/**")
                .addResourceLocations("/WEB-INF/static/images/");
        // 本次的应用没有引入分布式的文件存储单元,所以直接保村在WEB-INF之中
        registry.addResourceHandler("/yootk-upload/**")
                .addResourceLocations("/WEB-INF/upload/"); // 上传资源映射
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter =
                new MappingJackson2HttpMessageConverter(); // Jackson数据转换器
        CustomObjectMapper objectMapper = new CustomObjectMapper();
        converter.setObjectMapper(objectMapper);
        converter.setSupportedMediaTypes(List.of(
                MediaType.APPLICATION_JSON));
        converters.add(converter); // 追加转换器
    }
}

10、
package com.yootk.validate.web.config;

import com.yootk.validate.context.config.SpringApplicationContextConfig;
import com.yootk.validate.context.config.SpringWebContextConfig;
import jakarta.servlet.Filter;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import java.io.File;

public class StartWebApplication
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {SpringApplicationContextConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] {SpringWebContextConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }

    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter =
                new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        return new Filter[] {characterEncodingFilter};
    }

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        long maxFileSize = 2097152; // 单个文件最大为2M
        long maxRequestSize = 5242880; // 整个的请求最大为5M
        int fileSizeThreshold = 1048576; // 写入阈值
        File file = new File("/tmp"); // 创建保存目录
        if (!file.exists()) { // 路径不存在
            file.mkdirs(); // 创建目录
        }
        MultipartConfigElement element = new MultipartConfigElement(
                "/tmp", maxFileSize, maxRequestSize, fileSizeThreshold);
        registration.setMultipartConfig(element); // 定义上传配置
    }
}

请求包装

数据接收问题

  • 在 WEB 开发中,最为常用的参数传递模式,是通过地址重写或者是表单提交的方式进行传输,此时就可以直接依靠 HttpServletRequest 接口提供的 getParameterValue()以及 getParameterValues()方法实现请求数据的接收,但是如果此时用户传递的是个 JSON 数据内容,就需要考虑到数据接收问题了

请求接收

  • 如果此时要通过拦截器进行请求数据的验证,则必然要使用 HttpServletRequest 接口提供的 getlnputStream()方法进行请求主体数据的接收,这样才可以进行数据验证的处理,然而该数据流在整个的一次请求之中只允许读取一次。这样当数据验证通过后 SpringMVC 的内部会使用 Jackson 实现 JSON 请求数据与对象实例(方法参数上使用@RequestBody 注解)进行转换,所以此时会再次调用 getInputStream()方法,所以最终导致的问题就是 Jackson 处理时无法接收到所需的数据,也就自然无法实现 JSON 对象的转换处理。

自定义请求封装类

  • JakartaEE 为了解决这一设计问题,提供了 HttpServletRequestWrapper 包装类,开发者可以通过该类扩展一个自定义的请求处理类,并将用户所发送的主体数据直接保存在该类中,这样只要在后续的请求中传递自定义请求类的对象,就可以实现 getlnputStream()方法的重复调用
1、
package com.yootk.common.http.util;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import java.io.IOException;

public class ReadRequestBodyData { // 数据读取操作
    private ReadRequestBodyData() {} // 构造方法私有化
    public static byte[] getRequestBodyData(HttpServletRequest request) throws IOException { // 读取字节数据
        HttpServletRequestWrapper requestWrapper =
                new HttpServletRequestWrapper(request); // 包装请求
        int contentLength = requestWrapper.getContentLength(); // 数据长度
        if (contentLength < 0) { // 没有输入数据流
            return null; // 直接返回
        }
        byte buffer [] = new byte[contentLength]; // 开辟数组
        int len = 0; // 保存每次读取到的数据长度
        requestWrapper.getInputStream().read(buffer); // 数据读取
        return buffer;
    }
}


2、
package com.yootk.common.http;

import com.yootk.common.http.util.ReadRequestBodyData;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import java.io.ByteArrayInputStream;
import java.io.IOException;

public class YootkRequestWrapper extends HttpServletRequestWrapper { // 请求包装类
    private byte body[]; // 用户请求数据
    public YootkRequestWrapper(HttpServletRequest request) {
        super(request);
        try {
            this.body = ReadRequestBodyData.getRequestBodyData(request); // 数据读取
        } catch (IOException e) {}
    }

    @Override
    public ServletInputStream getInputStream() throws IOException { // 方法覆写
        ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() { // 重新实例化一个Servlet输入流
            private int temp = 0; // 定义标记
            @Override
            public boolean isFinished() {
                return temp != -1; // 读取到IO底部是-1
            }
            @Override
            public boolean isReady() {
                return true; // 可以读取
            }
            @Override
            public void setReadListener(ReadListener readListener) {}
            @Override
            public int read() throws IOException {
                temp = inputStream.read(); // 数据读取
                return temp;
            }
        };
    }
}


3、
package com.yootk.common.http.filter;

import com.yootk.common.http.YootkRequestWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

public class YootkStreamFilter implements Filter { // 自定义过滤器
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        if (httpServletRequest.getContentType() != null) { // 可能是其他类型
            if (httpServletRequest.getContentType()
                    .startsWith("application/json")) { // MIME类型判断
                ServletRequest requestWrapper = new YootkRequestWrapper(httpServletRequest);
                filterChain.doFilter(requestWrapper, servletResponse); // 更换request对象
            } else {
                filterChain.doFilter(servletRequest, servletResponse); // 普通处理
            }
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
}


4、
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter =
                new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        YootkStreamFilter streamFilter = new YootkStreamFilter(); // 请求包装过滤
        return new Filter[] {characterEncodingFilter, streamFilter};
    }

定义基础数据验证规则

拦截规则配置

  • 数据验证规则是实现拦截器验证处理的核心关键,在大部分的项目中都可能会包含有数字、日期、日期时间、布尔、数组等相关的验证需要,同时还需要考虑不同的应用场景所带来的一些特殊的验证规则配置,为了解决该类的设计问题,可以在项目中定义一个 IValidateRule 验证规则接口,而后不同的数据验证处理只需要实现该接口即可

1、
package com.yootk.common.validate;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.List;

public interface IValidateRule { // 数据验证接口
    public boolean validate(Object str); // 数据验证方法
    public String errorMessage(); // 返回验证错误信息

    /**
     * 方法实现请求参数的接收,但是现在的请求参数分为两种。
     * 一种是进行请求参数的处理,这种操作可以由request对象
     * 另外一种是Jackson的数据处理,所以这个时候要考虑到数据已经为对象的情况
     * @param param 可能是内容,也可能是对象,或者是普通参数名称
     * @return 参数对应的数据
     */
    public default String getParameterValue(Object param) { // 接收参数
        if (StringUtils.hasLength(request().getContentType())) {    // 有请求类型
            if (request().getContentType().startsWith(
                    MediaType.APPLICATION_JSON_VALUE)) {
                List<String> all = (List) param; // 接收数据
                return all.get(0); // 返回第一个数据
            }
            return param.toString(); // 直接返回原始的内容
        }
        return request().getParameter(param.toString());
    }
    public default String[] getParameterValues(Object param) {
        if (StringUtils.hasLength(request().getContentType())) {    // 有请求类型
            if (request().getContentType().startsWith(
                    MediaType.APPLICATION_JSON_VALUE)) {
                List<String> all = (List) param; // 接收数据
                return all.toArray(new String[] {}); // List转为String数组
            }
        }
        return request().getParameterValues(param.toString()); // 接收请求参数
    }
    public default HttpServletRequest request() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest(); // 获取当前请求对象
        return request;
    }
}


2、
package com.yootk.common.validate.rule.abs;

import com.yootk.common.validate.IValidateRule;

public abstract class AbstractValidateRule implements IValidateRule {
    @Override
    public String errorMessage() { // 作为公共的错误信息出现
        return "请求数据错误,无法通过验证,请确认数据内容是否正确!";
    }
}


3、

package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class StringValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        return StringUtils.hasLength(value); // 判断是否有内容
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据不允许为空!";
    }
}

4、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class BooleanValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        if (!StringUtils.hasLength(value)) {    // 数据为空
            return false;
        }
        if ("on".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value) ||
            "up".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value) ||
            "true".equalsIgnoreCase(value)) {
            return true;
        }
        return false;
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据必须是“true”或者是“false”";
    }
}


5、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class IntValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        if (!StringUtils.hasLength(value)) {    // 数据为空
            return false;
        }
        return value.matches("\\d+"); // 正则验证
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据必须是整数";
    }
}


6、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;

public class LongValidateRule extends IntValidateRule implements IValidateRule {
}


7、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class DoubleValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        if (!StringUtils.hasLength(value)) {    // 数据为空
            return false;
        }
        return value.matches("\\d+(\\.\\d+)?"); // 正则验证
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据必须是小数";
    }
}


8、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class DateValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        if (!StringUtils.hasLength(value)) {    // 数据为空
            return false;
        }
        return value.matches("\\d{4}-\\d{2}-\\d{2}"); // 正则验证
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据必须是日期格式(yyyy-MM-dd)";
    }
}


9、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class DatetimeValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        if (!StringUtils.hasLength(value)) {    // 数据为空
            return false;
        }
        return value.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"); // 正则验证
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据必须是日期时间格式(yyyy-MM-dd HH:mm:ss)";
    }
}

10、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class RandValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    public static final String RAND_SESSION_NAME = "rand"; // 验证码生成属性名称
    @Override
    public boolean validate(Object param) {
        String value = getParameterValue(param); // 获取请求参数
        if (!StringUtils.hasLength(value)) {    // 数据为空
            return false;
        }
        String rand = (String) request().getSession().getAttribute(RAND_SESSION_NAME);
        return value.equalsIgnoreCase(rand);
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求验证码不正确";
    }
}

11、

package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.util.StringUtils;

public class StringsValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String values [] = getParameterValues(param); // 接收请求参数
        return values != null; // 数组不能为空
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求数据不允许为空!";
    }
}


12、

package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;

public class IntsValidateRule extends AbstractValidateRule
    implements IValidateRule {  // 注意统一的后缀
    @Override
    public boolean validate(Object param) {
        String values [] = getParameterValues(param); // 接收请求参数
        if (values == null) {
            return false;
        } else {
            for (int x = 0; x < values.length; x++) {
                if (!values[x].matches("\\d+")) { // 数组项不是数字结构
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public String errorMessage() { // 继续扩展使用
        return "请求内容必须为数字!";
    }
}

13、
package com.yootk.common.validate.rule;

import com.yootk.common.validate.IValidateRule;

public class LongsValidateRule extends IntsValidateRule implements IValidateRule {
}


获取验证规则

定义验证注解

  • 完整的 WEB 应用会提供有大量的控制层处理方法,同时不同的控制层方法也会接收不同的请求参数,这样在进行控制层验证处理时,就需要开发者进行明确的验证规则的配置,而为了验证规则的维护可以通过自定义注解进行配置,在拦截处理时,可以通过 HandlerMethod 对象获取注解以实现请求数据验证
1、
package com.yootk.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD}) // 该注解只能够在方法上定义
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface RequestDataValidate { // 自定义注解
    // 只要配置上了此注解就表示当前控制层的方法上需要进行数据的验证处理
    boolean required() default true; // 是否要进行验证的标记
    // 注解之中编写value属性可以直接写上具体内容,从而避免了重复的属性名称定义
    String value() default ""; // 配置验证规则
}


2、
package com.yootk.validate.action;

import com.yootk.common.annotation.RequestDataValidate;
import com.yootk.common.web.action.abs.AbstractAction;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/pages/emp/") // 定义路径
public class EmpAction extends AbstractAction { // 定义Action处理类
    @RequestDataValidate
    public ModelAndView add() {
        return null;
    }
}


3、
package com.yootk.common.interceptor;

import com.yootk.common.annotation.RequestDataValidate;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

public class RequestDataValidateInterceptor implements HandlerInterceptor {
    private final static Logger LOGGER =
            LoggerFactory.getLogger(RequestDataValidateInterceptor.class);
    private boolean restSwith = false; // 是否使用REST结构

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) { // 对象类型
            // 根据当前调用的控制层的方法获取指定名称的Annotation注解
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RequestDataValidate validate =
                handlerMethod.getMethodAnnotation(RequestDataValidate.class);
            if (validate == null) { // 没有该注解
                return true; // 该操作不需要数据验证
            } else { // 如果注解配置了
                if (!validate.required()) {     // 关闭验证了
                    return true; // 请求直接转发
                } else {
                    String rules = validate.value(); // 获取配置验证规则
                    if (StringUtils.hasLength(rules)) { // 规则存在
                        LOGGER.debug("【{}()】{}",
                                handlerMethod.getMethod().getName(), rules);
                        // 此处暂时不进行任何规则的判断,只是做一个输出
                    }
                    return true;
                }
            }
        }
        return true;
    }
}


4、
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        RequestDataValidateInterceptor interceptor =
                new RequestDataValidateInterceptor(); // 请求数据验证拦截器
        interceptor.setRestSwith(false); // 拦截器显示风格
        registry.addInterceptor(interceptor).addPathPatterns("/pages/**");
    }

5、
package com.yootk.validate.vo;

import java.util.Date;
import java.util.Set;

public class Emp {
    private Long empno;
    private String ename;
    private java.util.Date hiredate;
    private Double sal;
    private Set<String> roles;
    public Long getEmpno() {
        return empno;
    }
    public void setEmpno(Long empno) {
        this.empno = empno;
    }
    public String getEname() {
        return ename;
    }
    public void setEname(String ename) {
        this.ename = ename;
    }
    public Date getHiredate() {
        return hiredate;
    }
    public void setHiredate(Date hiredate) {
        this.hiredate = hiredate;
    }
    public Double getSal() {
        return sal;
    }
    public void setSal(Double sal) {
        this.sal = sal;
    }
    public Set<String> getRoles() {
        return roles;
    }
    public void setRoles(Set<String> roles) {
        this.roles = roles;
    }
}


6、
package com.yootk.validate.action;

import com.yootk.common.annotation.RequestDataValidate;
import com.yootk.common.web.action.abs.AbstractAction;
import com.yootk.validate.vo.Emp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.Arrays;

@Controller
@RequestMapping("/pages/emp/") // 定义路径
public class EmpAction extends AbstractAction { // 定义Action处理类
    private static final Logger LOGGER =
            LoggerFactory.getLogger(EmpAction.class);
    @PostMapping("add")
    // 所有的规则按照”参数名称:规则标记;参数名称:规则标记;参数名称:规则标记;“结构定义
    @RequestDataValidate(
            "empno:long;ename:string:sal:double;hiredate:date;roles:strings")
    public ModelAndView add(Emp emp) {
        LOGGER.info("【增加雇员数据】雇员编号:{}、姓名:{}、工资:{}、雇佣日期:{}、角色:{}",
                emp.getEmpno(), emp.getEname(), emp.getSal(),
                emp.getHiredate(), emp.getRoles());
        return null;
    }
    @PostMapping("edit")
    @RequestDataValidate(required = true,
            value = "empno:long;ename:string:sal:double;hiredate:date;roles:strings")
    public ModelAndView edit(@RequestBody Emp emp) {
        LOGGER.info("【更新雇员数据】雇员编号:{}、姓名:{}、工资:{}、雇佣日期:{}、角色:{}",
                emp.getEmpno(), emp.getEname(), emp.getSal(),
                emp.getHiredate(), emp.getRoles());
        return null;
    }
    @GetMapping("get")
    @RequestDataValidate(required = false, value = "empno:long")
    public ModelAndView get(long empno) {
        LOGGER.info("【查询雇员信息】雇员编号:{}", empno);
        return null;
    }
    @DeleteMapping("delete")
    @RequestDataValidate("ids:longs")
    public ModelAndView delete(long ids[]) {
        LOGGER.info("【删除雇员信息】雇员编号:{}", Arrays.toString(ids));
        return null;
    }
}



7、
curl -X POST -d "empno=7369&ename=smith&sal=2450&hiredate=1979-09-19&roles=news&roles=system&roles=message" "http://localhost/pages/emp/add"

8、
curl -X GET "http://localhost/pages/emp/get?empno=7369"

9、
curl -X POST "http://localhost/pages/emp/edit" -H "Content-Type:application/json;charset=UTF-8" -d "{\"empno\":\"7369\",\"ename\":\"Smith\",\"hiredate\":\"1969-09-19\",\"sal\":\"800\",\"roles\":[\"news\",\"system\",\"message\"]}"
curl -X POST "http://localhost/pages/emp/edit" -H "Content-Type:application/json;charset=utf-8" -d "{\"empno\":\"7369\",\"ename\":\"Smith\",\"hiredate\":\"1969-09-19\",\"sal\":\"800\",\"roles\":[\"news\",\"system\",\"message\"]}"

10、
curl -X DELETE "http://localhost/pages/emp/delete?ids=7369&ids=7566&ids=7839"

数据验证处理

数据验证处理

  • 在使用@RequestDataValidate 注解进行请求拦截规则配置时,所有的规则采用的都是“参数名称:验证类型”的形式传递的,而后多个验证规则之间使用分号";"分割,这为了程序时就可以针对于不同的规则调用 IValidateRule 接口的实现类进行验证处理。设计结构的管理,可以在定义一个 ValidateUtils 工具类,以封装验证操作的处理逻辑
1、
package com.yootk.common.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yootk.common.http.util.ReadRequestBodyData;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ValidateUtils { // 实现数据验证的处理
    private final static Logger LOGGER =
            LoggerFactory.getLogger(ValidateUtils.class);
    private ValidateUtils() {} // 不需要额外产生任何的对象

    /**
     * 对指定配置的规则进行数据的验证处理操作,但是这个验证处理会返回错误信息
     * @param rules 之前获取到的验证规则的字符串,字符串的结构“参数:规则;参数:规则;..”形式定义
     * @return 最终的验证的结果,而这些结果保存的就是错误信息,这个错误信息在进行保存的时候采用的结构
     * key = 参数名称、value = 错误信息,如果Map集合长度为0则表示没有任何的错误
     */
    public static Map<String, String> validate(String rules) {
        // 错误信息是在IValidateRule接口之中的getErrorMessage()方法返回的
        Map<String, String> result = new HashMap<>();
        String ruleArray [] = rules.split(";"); // 进行数据的拆分处理
        for (String rule : ruleArray) { // 数组迭代
            // 每一个rule是一组规则,而这个规则的组成:“参数名称:规则”
            String temp [] = rule.split(":"); // 索引0是参数名称,所以1是规则信息
            Map<String, Object> validateResult = validate(temp[0], temp[1]); // 数据验证
            if (validateResult.containsKey("flag")) { // 包含有选项再进行处理
                boolean flag = (Boolean) validateResult.get("flag"); // 获取验证结果
                if (!flag) {    // 此时验证失败了
                    // temp[0]拆分的结果为当前请求参数的名称
                    result.put(temp[0], (String) validateResult.get("message"));
                }
            }
        }
        return result; // 返回验证结果
    }

    /**
     * RESTful数据请求的验证处理逻辑
     * @param request 当前的请求对象(已经做了一个Request包装处理)
     * @param rules 所获取到的验证规则
     * @return 最终的验证的结果,而这些结果保存的就是错误信息,这个错误信息在进行保存的时候采用的结构
     * key = 参数名称、value = 错误信息,如果Map集合长度为0则表示没有任何的错误
     */
    public static Map<String, String> validateBody(
            HttpServletRequest request, String rules) {
        Map<String, String> result = new HashMap<>(); // 保存验证的结果
        ObjectMapper mapper = new ObjectMapper(); // Jackson工具类处理
        try {
            // 当前如果所接收到的是一个JSON数据,那么就需要进行转换,但是数据是需要验证的,把它转为Map集合
            Map<String, List<String>> jsonMap = mapper.readValue(
                    ReadRequestBodyData.getRequestBodyData(request), Map.class);
            String ruleArray [] = rules.split(";"); // 数据拆分
            for (String rule : ruleArray) { // 迭代每一行的规则“参数名称:规则标记“
                String temp [] = rule.split(":"); // 规则拆分
                // 此时传递到验证方法之中的是具体的对象的数据(List集合)
                Map<String, Object> validateResult = validate(jsonMap.get(temp[0]), temp[1]);
                boolean flag = (Boolean) validateResult.get("flag"); // 获取验证结果
                if (!flag) {    // 此时验证失败了
                    // temp[0]拆分的结果为当前请求参数的名称
                    result.put(temp[0], (String) validateResult.get("message"));
                }
            }
        } catch (IOException e) {
        }
        return result;
    }
    /**
     * 根据指定的参数和规则进行数据的验证
     * @param param 参数名称,也有可能是具体的内容
     * @param rule 规则名称
     * @return 返回数据的信息,这个信息包含有两类的数据
     * key = flag、value = Boolean验证结果(true或false)
     * key = message、value = 验证失败时的错误信息(IValidateRule.errorMessage()方法结果)
     */
    public static Map<String, Object> validate(Object param, String rule) {
        LOGGER.debug("【数据验证】param = {}、rule = {}", param, rule);
        Map<String, Object> result = new HashMap<>(); // 保存验证结果
        // 之所以现在这么做,是因为所有的程序是由用户自己开发的,如果你真的是交给其他使用者,包名称肯定不相同
        // 整体的设计可以更换为基于注解配置的方式进行处理
        String validateClass = "com.yootk.common.validate.rule." +
                StringUtils.capitalize(rule) + "ValidateRule"; // 验证处理类
        Class<?> clazz = null; // 保存当前要操作的规则处理类
        try {
            clazz = Class.forName(validateClass); // 获取当前的规则处理类
        } catch (ClassNotFoundException e) {
            return result; // 直接停止后续调用了
        }
        LOGGER.debug("【获取数据验证类】{}", clazz.getName());
        // 当前所使用的操作结构全部为Spring内部提供的处理类,相关的使用介绍Spring源代码中分析过了
        Method validateMethod = BeanUtils.findMethod(clazz, "validate", Object.class); // 获取方法实例
        Object obj = BeanUtils.instantiateClass(clazz); // 对象实例化
        try { // 现在通过验证方法进行当前规则的方法调用
            boolean flag = (Boolean) validateMethod.invoke(obj, param); // 反射方法调用
            result.put("flag", flag); // 保存数据验证的结果
            if (!flag) { // 当前的验证未通过
                Method messageMethod = BeanUtils.findMethod(clazz, "errorMessage"); // 获取错误信息
                result.put("message", messageMethod.invoke(obj)); // 保存错误信息
            }
            return result; // 返回最终的处理结果
        } catch (Exception e) {
            return result;
        }
    }
}


2、
package com.yootk.common.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;

import java.io.IOException;
import java.util.Map;

public class ResponseHandler { // 创建一个数据响应操作类
    public static void responseError(Map<String, String> result, boolean restSwitch, HandlerMethod handlerMethod) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getResponse(); // 获取当前请求对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest(); // 获取当前请求对象
        if (restSwitch) { // 当前为REST请求处理
            response.setContentType(MediaType.APPLICATION_JSON_VALUE); // 依据JSON显示错误
            response.setCharacterEncoding("UTF-8");
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 设置状态码
            try {
                response.getWriter().print(new ObjectMapper().writeValueAsString(result)); // JSON响应
            } catch (IOException e) {}
        } else { // 如果此时没有错误
            try {
                request.getRequestDispatcher("/errors").forward(request, response);
            } catch (Exception e) {}
        }
    }
}


3、
package com.yootk.common.interceptor;

import com.yootk.common.annotation.RequestDataValidate;
import com.yootk.common.util.ResponseHandler;
import com.yootk.common.util.ValidateUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

public class RequestDataValidateInterceptor implements HandlerInterceptor {
    private final static Logger LOGGER =
            LoggerFactory.getLogger(RequestDataValidateInterceptor.class);
    private boolean restSwith = false; // 是否使用REST结构

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) { // 对象类型
            // 根据当前调用的控制层的方法获取指定名称的Annotation注解
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RequestDataValidate validate =
                handlerMethod.getMethodAnnotation(RequestDataValidate.class);
            if (validate == null) { // 没有该注解
                return true; // 该操作不需要数据验证
            } else { // 如果注解配置了
                if (!validate.required()) {     // 关闭验证了
                    return true; // 请求直接转发
                } else {
                    String rules = validate.value(); // 获取配置验证规则
                    if (StringUtils.hasLength(rules)) { // 规则存在
                        LOGGER.debug("【{}()】{}",
                                handlerMethod.getMethod().getName(), rules);
                        boolean requestBodyFlag = false; // 是否存在有指定注解
                        for (MethodParameter parameter : handlerMethod.getMethodParameters()) { // 循环方法参数
                            // 获取方法参数上带有@RequestBody注解的判断
                            RequestBody requestBody = parameter.getParameterAnnotation(RequestBody.class);
                            if (requestBody != null) { // 存在有指定的注解
                                requestBodyFlag = true; // 要采用REST架构进行参数的接收
                            }
                        }
                        Map<String, String> result = null; // 保存数据验证结果
                        if (requestBodyFlag) {  // 当前要使用JSON处理
                            result = ValidateUtils.validateBody(request, rules); // 调用数据验证
                        } else {    // 如果当前为普通的参数
                            result = ValidateUtils.validate(rules); // 普通验证
                        }
                        LOGGER.debug("【数据验证】{}", result);
                        if (result != null && result.size() > 0) { // 此时存在有错误信息
                            ResponseHandler.responseError(result, this.restSwith, handlerMethod);
                            return false; // 不转发请求
                        }
                    }
                    return true;
                }
            }
        }
        return true;
    }
    public void setRestSwith(boolean restSwith) { // 考虑后续的错误显示问题
        this.restSwith = restSwith;
    }
}


4、
curl -X POST -d "empno=7369&ename=smith&sal=2450&hiredate=1979-09-19&roles=news&roles=system&roles=message" "http://localhost/pages/emp/add"

curl -X POST -d "empno=736ss9&ename=&sal=245s0&hiredate=19791-09-19" "http://localhost/pages/emp/add"

5、
curl -X POST "http://localhost/pages/emp/edit" -H "Content-Type: application/json;charset=utf-8" -d "{\"empno\": \"7369\", \"ename\": \"Smith\", \"hiredate\": \"1969-09-19\", \"sal\": \"800\", \"roles\" : [\"news\", \"system\",\"message\"]}"
curl -X POST "http://localhost/pages/emp/edit" -H "Content-Type: application/json;charset=utf-8" -d "{\"empno\": \"736ss9\", \"ename\": \"Smith\", \"hiredate\": \"19691-09-19\", \"sal\": \"8s00\", \"roles\" : [\"news\", \"system\",\"message\"]}"

curl -X POST "http://localhost/pages/emp/edit" -H "Content-Type: application/json;charset=utf-8" -d "{\"empno\": \"7369\", \"ename\": \"Smith\", \"hiredate\": \"1969-09-19\", \"sal\": \"80sss0\"}"

6、
【edit()】empno:long;ename:string;sal:double;hiredate:date;roles:strings
【数据验证】param = 7369、rule = long
【获取数据验证类】com.yootk.common.validate.rule.LongValidateRule
【数据验证】param = Smith、rule = string
【获取数据验证类】com.yootk.common.validate.rule.StringValidateRule
【数据验证】param = 800、rule = double
【获取数据验证类】com.yootk.common.validate.rule.DoubleValidateRule
【数据验证】param = 1969-09-19、rule = date
【获取数据验证类】com.yootk.common.validate.rule.DateValidateRule
【数据验证】param = [news, system, message]、rule = strings
【获取数据验证类】com.yootk.common.validate.rule.StringsValidateRule
【数据验证】{}
【更新雇员数据】雇员编号:7369、姓名:Smith、工资:800.0、雇佣日期:Fri Sep 19 00:00:00 CST 1969、角色:[news, system, message]

错误展示

错误信息展示

  • 拦截器实现了请求数据的验证处理,而在验证完成之后,就需要对相应的错误信息进行展示,考虑到程序结构化设计的要求,本次将通过 ResponseHandler 处理类进行错误显式的处理

数据验证处理

  • 在当前的项目设计中,由于存在有前后端分离的设计需要,所以在进行错误信息处理时就需要满足 REST 风格以及传统 Dispatcher 页面重定向两种方式。如果是 REST 风格显式则可以通过 Jackson 工具类将错误信息转化为 JSON 数据,随后直接通过 HttpServletResopnse 接口实例进行响应,而如果是 Dispatcher 风格的处理,则让其跳转到指定的错误路径。
  • 由于不同的控制层方法会存在有不同的错误显式路径,所以对于错误路径的配置,可以创建一个自定义的@ErrorPage 注解,在控制层方法定义时,通过此注解配置其专属的错误处理路径。同时考虑到代码配置的灵活性,也可以在没有配置错误处理路径时,让其统一跳转到“/error”路径进行错误显式。

WEB 请求与数据验证处理流程

  • 在执行 add0 方法之前,会根据该方法中所配置的@RequestDataValidate 注解进行数据验证,如果验证失败,,则会跳转到@ErrorPage 注解所配置的错误页进行错误显式,如果没有配置具体的错误页,则会统一跳转到"/error”路径进行处理,本程序也对这一路径进行了显式实现。当用户数据输入正确后,会跳转到“/EB-INF/pages/emp/emp add success.jsp”页面进行信息显式
1、
package com.yootk.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD}) // 只允许在方法上定义错误页
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface ErrorPage { // 定义错误页
    String value() default "/error"; // 默认错误页路径
}


2、
package com.yootk.common.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yootk.common.annotation.ErrorPage;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;

import javax.xml.crypto.Data;
import java.io.IOException;
import java.util.Map;

public class ResponseHandler { // 创建一个数据响应操作类
    public static final String ERROR_PAGE_PATH = "/error"; // 默认错误页
    public static void responseError(Map<String, String> result, boolean restSwitch, HandlerMethod handlerMethod) {
        if (restSwitch) { // 当前为REST请求处理
            restResponse(result);
        } else { // 如果此时没有错误
            dispatcherResponse(result, handlerMethod);
        }
    }
    public static void restResponse(Map<String, String> result) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getResponse(); // 获取当前请求对象
        response.setContentType(MediaType.APPLICATION_JSON_VALUE); // 依据JSON显示错误
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 设置状态码
        try {
            response.getWriter().print(new ObjectMapper().writeValueAsString(result)); // JSON响应
        } catch (IOException e) {}
    }
    public static void dispatcherResponse(Map<String, String> result,
                                          HandlerMethod handlerMethod) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getResponse(); // 获取当前请求对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest(); // 获取当前请求对象
        String errorPath = null;// 错误的跳转路径
        ErrorPage errorPage = handlerMethod.getMethodAnnotation(ErrorPage.class); // 获取错误页
        if (errorPage != null) {
            errorPath = errorPage.value(); // 获取错无页
        } else { // 没有配置错误页
            errorPath = ERROR_PAGE_PATH; // 默认错误页
        }
        try {
            // 所有的错误信息都使用Map<String, String>的结构进行传递
            request.setAttribute("errors", result); // 传递错误
            request.getRequestDispatcher("/errors").forward(request, response);
        } catch (Exception e) {}
    }
}


3、
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head><title>SSM实战手册</title></head>
<body>
    <img src="/yootk-images/error/code_500.png">
    <c:forEach items="${errors}" var="error">
        <li><span style="font:30px;">${error.key} = ${error.value}</span></li>
    </c:forEach>
</body>
</html>

package com.yootk.validate.action;

import com.yootk.common.web.action.abs.AbstractAction;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ErrorAction extends AbstractAction { // 公共的错误路径
    @RequestMapping("/error")
    public ModelAndView error() {
        return new ModelAndView("/error/error_500");
    }
    @RequestMapping("/notfound")
    public ModelAndView notfound() {
        return new ModelAndView("/error/error_404");
    }
}


4、
curl -X POST -d "empno=736ss9&ename=smith&sal=2450&hiredate=1979-09-19&roles=news&roles=system&roles=message" "http://localhost/pages/emp/add"
curl -X POST "http://localhost/pages/emp/edit" -H "Content-Type: application/json;charset=utf-8" -d "{\"empno\": \"7369\", \"ename\": \"Smith\", \"hiredate\": \"1969-09-19\", \"sal\": \"80sss0\", \"roles\" : [\"news\", \"system\",\"message\"]}"


5、
package com.yootk.validate.action.advice;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ErrorAdvice {
    @ExceptionHandler(Exception.class)
    public ModelAndView handle(Exception e, HttpServletRequest request,
                               HttpServletResponse response) {
        ModelAndView mav = new ModelAndView("forward:/error"); // 跳转到指定的Action
        Map<String, String> result = new HashMap<>();
        result.put("message", e.getMessage());
        result.put("type", e.getClass().getName());
        result.put("path", request.getRequestURI());
        result.put("referer", request.getHeader("Referer"));
        mav.addObject("errors", result);
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return mav;
    }
}


6、

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head><title>SSM实战手册</title></head>
<body>
<c:if test="${flag}">
    <div>雇员信息增加成功!</div>
    <div>
        <li>雇员编号:${emp.empno}</li>
        <li>雇员姓名:${emp.ename}</li>
        <li>基本工资:${emp.sal}</li>
        <li>雇佣日期:<fmt:formatDate value="${emp.hiredate}" pattern="yyyy年MM月dd日"/></li>
        <li>角色配置:${emp.roles}</li>
    </div>
</c:if>
</body>
</html>
7、
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head><title>SSM实战手册</title></head>
<body>
<form action="/pages/emp/add" method="post">
    雇员编号:<input type="text" name="empno" value="7369"/>${errors['empno']}<br/>
    雇员姓名:<input type="text" name="ename" value="李兴华"/>${errors['ename']}<br/>
    基本工资:<input type="text" name="sal" value="2450.79"/>${errors['sal']}<br/>
    雇佣日期:<input type="date" name="hiredate" value="1999-09-19"/>${errors['hiredate']}<br/>
    角色配置:<input type="checkbox" name="roles" value="news"/>新闻管理
               <input type="checkbox" name="roles" value="system" checked/>系统管理
               <input type="checkbox" name="roles" value="message" checked/>消息管理
                ${errors['roles']}<br/>
    <button type="submit">增加</button><button type="reset">重置</button>
</form>
</body>
</html>

8、
    @RequestMapping("add_input")
    public String addInput() {
        return "/emp/emp_add_input"; // 跳转到表单页
    }
    @ErrorPage("/pages/emp/add_input") // 控制层路径
    @PostMapping("add")
    // 所有的规则按照”参数名称:规则标记;参数名称:规则标记;参数名称:规则标记;“结构定义
    @RequestDataValidate(
            "empno:long;ename:string;sal:double;hiredate:date;roles:strings")
    public ModelAndView add(Emp emp) {
        LOGGER.info("【增加雇员数据】雇员编号:{}、姓名:{}、工资:{}、雇佣日期:{}、角色:{}",
                emp.getEmpno(), emp.getEname(), emp.getSal(),
                emp.getHiredate(), emp.getRoles());
        ModelAndView mav = new ModelAndView("/emp/emp_add_success");
        mav.addObject("emp", emp); // 传递对象
        mav.addObject("flag", true); // 传递标记
        return mav;
    }

9、
localhost/pages/emp/add_input

上传文件验证

MultipartHttpServletRequest 请求包装

  • 上传是 HTTP 的核心功能,也是很多应用开发中一定会使用到的实现技术,SpringMVC 可以直接整合 JakartaEE 中的上传支持进行上传文件的接收,但是在项目的开发之中不可能让用户随意上传文件,所以也需要对用户上传文件的类型加以限制,考虑到已有数据验证结构的定义,所以本次将继续通过 IValidateRule 接口实现校验。
1、
package com.yootk.common.http.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;

import java.io.IOException;

public class MultipartRequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        if (httpServletRequest.getContentType() != null) { // 判断请求类型
            if (httpServletRequest.getContentType()
                    .startsWith("multipart/form-data")) { // 类型判断
                StandardMultipartHttpServletRequest multiRequest =
                        new StandardMultipartHttpServletRequest(httpServletRequest);
                filterChain.doFilter(multiRequest, servletResponse);
            } else {
                filterChain.doFilter(servletRequest, servletResponse);
            }
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
}


2、
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter =
                new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        YootkStreamFilter streamFilter = new YootkStreamFilter(); // 请求包装过滤
        MultipartRequestFilter multiFilter = new MultipartRequestFilter();
        return new Filter[] {characterEncodingFilter, streamFilter, multiFilter};
    }

3、
package com.yootk.common.util;

import org.springframework.web.method.HandlerMethod;

public class HandlerMethodStorageUtils { // 存储工具
    private static final ThreadLocal<HandlerMethod> THREAD_LOCAL_STORAGE = new ThreadLocal<>();
    public static void set(HandlerMethod method) {
        THREAD_LOCAL_STORAGE.set(method);
    }
    public static HandlerMethod get() {
        return THREAD_LOCAL_STORAGE.get();
    }
}


4、
package com.yootk.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER}) // 参数上生效
@Retention(RetentionPolicy.RUNTIME)
public @interface UploadFileType { // 允许上传文件的类型
    // 配置允许上传文件的类型
    public String value() default "image/bmp;image/gif;image/jpg;image/jpeg;image/png";
}


5、
package com.yootk.common.validate.rule;

import com.yootk.common.annotation.UploadFileType;
import com.yootk.common.util.HandlerMethodStorageUtils;
import com.yootk.common.validate.IValidateRule;
import com.yootk.common.validate.rule.abs.AbstractValidateRule;
import org.springframework.core.MethodParameter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;

import java.util.Map;
import java.util.Set;

public class UploadValidateRule extends AbstractValidateRule implements IValidateRule {
    private Set<String> mimeType = null; // 保存支持的MIME类型
    @Override
    public boolean validate(Object str) {
        if (!(request() instanceof StandardMultipartHttpServletRequest)) { // 如果不为上传
            return true;
        }
        if (this.isValidate()) {    // 需要进行MIME类型的验证
            StandardMultipartHttpServletRequest multiRequest =
                    (StandardMultipartHttpServletRequest) request(); // 获取请求
            Map<String, MultipartFile> fileMap = multiRequest.getFileMap(); // 获取全部的上传文件
            MultipartFile file = fileMap.get(str); // 获取指定的上传文件
            if (file != null && file.getSize() > 0) {
                return mimeType.contains(file.getContentType()); // 是否包含有指定的类型
            } else {
                return true;
            }
        }
        return true;
    }
    public boolean isValidate() { // 判断当前是否需要验证
        boolean updateFlag = false; // 做一个执行的标记
        if (request().getContentType() != null) { // 判断请求类型
            if (request().getContentType()
                    .startsWith("multipart/form-data")) { // 文件上传类型
                HandlerMethod handlerMethod = HandlerMethodStorageUtils.get(); // 获取实例
                for (MethodParameter parameter : handlerMethod.getMethodParameters()) { // 迭代参数
                    UploadFileType uploadFileType = parameter.getParameterAnnotation(UploadFileType.class);
                    if (uploadFileType != null) { // 已配置注解
                        String mime = uploadFileType.value(); // 获取配置的MIME类型
                        if (mime != null) {
                            this.mimeType = Set.of(uploadFileType.value().split(";"));
                            updateFlag = true; // 需要上传验证
                            break;
                        }
                    }
                }
            }
        }
        return updateFlag;
    }

    @Override
    public String errorMessage() {
        if (this.mimeType != null && this.mimeType.size() > 0) {
            return "上传文件类型不匹配,可选类型为:" + this.mimeType;
        } else {
            return "上传文件错误,无法接收请求!";
        }
    }
}


6、
package com.yootk.common.http.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.util.UUID;

public class UploadFileUtils { // 上传工具类
    private static final Logger LOGGER = LoggerFactory.getLogger(UploadFileUtils.class);
    public static final String NO_PHOTO = "nophoto.png";

    /**
     * 上传文件存储
     * @param file 要存储的文件
     * @param saveDir 保存的目录
     * @return UUID生成的文件名称
     * @throws Exception 文件存储异常
     */
    public static String save(MultipartFile file, String saveDir) throws Exception {
        if (file == null || file.getSize() == 0) {  // 没有文件上传
            return NO_PHOTO;
        }
        // MIME类型:image/png
        String fileName = UUID.randomUUID() + "." + file.getContentType()
                .substring(file.getContentType().lastIndexOf("/") + 1);
        LOGGER.info("生成文件名称:{}", fileName);
        String filePath = ContextLoader.getCurrentWebApplicationContext()
                .getServletContext().getRealPath(saveDir) + File.separator + fileName;
        LOGGER.info("文件存储路径:{}", filePath);
        file.transferTo(new File(filePath)); // 文件转存
        return fileName; // 返回文件名称
    }
}


7、
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head><title>SSM实战手册</title></head>
<body>
<form action="/pages/emp/add" method="post" enctype="multipart/form-data">
    雇员编号:<input type="text" name="empno" value="7369"/>${errors['empno']}<br/>
    雇员姓名:<input type="text" name="ename" value="李兴华"/>${errors['ename']}<br/>
    基本工资:<input type="text" name="sal" value="2450.79"/>${errors['sal']}<br/>
    雇佣日期:<input type="date" name="hiredate" value="1999-09-19"/>${errors['hiredate']}<br/>
    角色配置:<input type="checkbox" name="roles" value="news"/>新闻管理
            <input type="checkbox" name="roles" value="system" checked/>系统管理
            <input type="checkbox" name="roles" value="message" checked/>消息管理
                ${errors['roles']}<br/>
    雇员照片:<input type="file" name="photo">${errors['photo']}<br/>
    <button type="submit">增加</button><button type="reset">重置</button>
</form>
</body>
</html>

8、
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head><title>SSM实战手册</title></head>
<body>
<c:if test="${flag}">
    <div>雇员信息增加成功!</div>
    <div>
        <li>雇员编号:${emp.empno}</li>
        <li>雇员姓名:${emp.ename}</li>
        <li>基本工资:${emp.sal}</li>
        <li>雇佣日期:<fmt:formatDate value="${emp.hiredate}" pattern="yyyy年MM月dd日"/></li>
        <li>角色配置:${emp.roles}</li>
        <li>雇员照片:<img src="/yootk-upload/${photo}" style="50px;"/></li>
    </div>
</c:if>
</body>
</html>

9、
    @ErrorPage("/pages/emp/add_input") // 控制层路径
    @PostMapping("add")
    // 所有的规则按照”参数名称:规则标记;参数名称:规则标记;参数名称:规则标记;“结构定义
    @RequestDataValidate(
            "empno:long;ename:string;sal:double;hiredate:date;roles:strings;photo:upload")
    public ModelAndView add(Emp emp, @UploadFileType MultipartFile photo) {
        LOGGER.info("【增加雇员数据】雇员编号:{}、姓名:{}、工资:{}、雇佣日期:{}、角色:{}",
                emp.getEmpno(), emp.getEname(), emp.getSal(),
                emp.getHiredate(), emp.getRoles());
        ModelAndView mav = new ModelAndView("/emp/emp_add_success");
        mav.addObject("emp", emp); // 传递对象
        mav.addObject("flag", true); // 传递标记
        String saveFileName = null; // 保存文件名称
        try {
            saveFileName = UploadFileUtils.save(photo, "/WEB-INF/upload"); // 文件保存
        } catch (Exception e) {}
        LOGGER.info("【文件上传】ContentType:{}、OriginalFileName:{}、Size:{}、SaveFileName:{}",
                photo.getContentType(), photo.getOriginalFilename(),
                photo.getSize(), saveFileName);
        mav.addObject("photo", saveFileName); // 用于图像的显示
        return mav;
    }


10、
http://localhost/pages/emp/add_input

demo


上次编辑于: