MVC功能配置示例

1.返回xml格式数据

实现返回xml格式数据而不是json,只需导入一个依赖

如果想要响应xml,需添加以下依赖

<!-- 此处需要导入databind包即可, jackson-annotations、jackson-core都不需要显示自己的导入了-->
<dependency>
 	<groupId>com.fasterxml.jackson.core</groupId>
 	<artifactId>jackson-databind</artifactId>
 	<!--<version>2.9.8</version>-->
</dependency>

<!-- jackson默认只会支持的json。若要xml的支持,需要额外导入如下包 -->
<dependency>
 	<groupId>com.fasterxml.jackson.dataformat</groupId>
 	<artifactId>jackson-dataformat-xml</artifactId>
 	<!--<version>2.9.8</version>-->
</dependency>

2.请求路径匹配配置

添加访问路径前缀

配置

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 路径匹配相关配置
     * */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        //为指定Controller的访问添加路径前缀
        configurer.addPathPrefix("/api/v1", 
                                 HandlerTypePredicate.forAssignableType(IndexController.class));
        
        //HandlerTypePredicate.forAssignableType-为某个处理器类设置访问前缀
        //HandlerTypePredicate.forAnnotation-为带有某些注解的处理器类设置访问前缀
        //HandlerTypePredicate.forBasePackageClass-为指定处理器类所在包及其子包中的所有处理器类设置访问前缀
        //HandlerTypePredicate.forBasePackage-为指定包及其子包中的所有处理器类设置访问前缀,参数为包路径字符串
        //HandlerTypePredicate.forAnyHandlerType-为所有处理器类设置访问前缀
    }
}

运行结果

Controller

@RestController
public class IndexController {

    @RequestMapping("/dem")
    public Demo demo( @RequestParam(value = "age") String age){
        System.out.println(age);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");
        return d;
    }
    
}

浏览器访问:

http://localhost:8081/api/v1/dem?age=12

image-20231017095459006

如果是:

http://localhost:8081/dem?age=12

就404了

尾部斜杠

默认情况下尾部斜杠为启用状态。

即:“/dem/1/” 与 “/dem/1"都会进入”/dem/{age}"的Controller处理,

如果禁用,则"/dem/1/“不会被”/dem/{age}"的Controller处理

配置

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 路径匹配相关配置
     * */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        //默认为true,改为false,也就是关闭尾部斜杠
        configurer.setUseTrailingSlashMatch(false);
    }
}

效果展示

Controller

@RestController
public class IndexController {

    @RequestMapping("/dem/{age}")
    public Demo demo(@PathVariable String age){
        System.out.println(age);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");
        return d;
    }
}

没配置前

image-20231017111809183

配置后

image-20231017111707366

3.内容协商

可以理解为客户端想要什么类型的数据,服务端就返回什么类型的数据。

如果是application/jsonapplication/xml,不需要任何配置,只需要设置请求头Accept,即可实现jsonxml的返回,如果是自定义媒体类型,则需要手动写代码进行配置

下面展示内容协商相关功能使用:

自定义媒体类型

就是客户端想要什么类型的数据。服务端就返回什么类型的数据,不固定于jsonxml。这里演示一个自定义媒体类型Demo的配置使用

步骤:

  1. 写mvc配置
  2. 自定义消息转换器,添加到mvc配置

配置

下面的配置主要有两个:

开启支持扩展名功能:configurer.favorParameter(true)

设置内容协商的策略:configurer.strategies(List.......)

@Configuration
public class webConfig implements WebMvcConfigurer {
    
    /**
     * 内容协商相关配置
     * */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer)  {
        /**
		 *	开启支持扩展名功能:
		 *	在请求url后添加 ?format=json 或者 ?format=xml,将返回json或xml
		 *  (这个扩展名支持也可以在yaml中设置,下面贴出)
		 */
        configurer.favorParameter(true);

        
        //支持的媒体类型
        Map<String, MediaType> mediaTypes = new HashMap<>();
        mediaTypes.put("json",MediaType.APPLICATION_JSON);
        mediaTypes.put("xml",MediaType.APPLICATION_XML);
        //新增自定义Demo媒体类型
        mediaTypes.put("demo",MediaType.parseMediaType("application/x-demo"));
       
        //指定支持解析哪些参数对应的哪些媒体类型,也就是内容协商策略,
        //这里是请求路径参数策略,即请求url后添加 ?format=json 或者 ?format=xml
        ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
 
        
		/**
		 *	设置内容协商的策略:
		 *		HeaderContentNegotiationStrategy: 请求头Accept
		 *		ParameterContentNegotiationStrategy: format路径参数
		 *		.........
		 */
        configurer.strategies(Arrays.asList(parameterStrategy));
        
        /**
		 *	可以同时设置多种策略,但是如果有HeaderContentNegotiationStrategy(请求头Accept策略),
		 *	则请求头Accept必须有自定义类型application/x-demo,而没有手动设置情况下,浏览器默认的发送的请求中,
		 *  请求头Accept并不会自带自定义媒体类型
		 */
        //configurer.strategies(Arrays.asList(new HeaderContentNegotiationStrategy(),parameterStrategy));
    }


    //添加自定义消息转换器
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MessageConverterForDemo());
    }

}

yaml配置的扩展名支持:

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

自定义消息转换器

public class MessageConverterForDemo implements HttpMessageConverter<Demo> {

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }
	
    //消息转换器可以写出的类
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return clazz.isAssignableFrom(Demo.class);
    }
	
    //消息转换器支持的媒体类型
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-demo");
    }

    @Override
    public Demo read(Class<? extends Demo> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }
	
    //写出方法
    @Override
    public void write(Demo demo, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //自定义数据格式的写出
        String data = demo.getOne() + ";" + demo.getTwo() + ";" + demo.getThree()  + ";" + demo.getFour();
        //写出去
        OutputStream body = outputMessage.getBody();
        body.write(data.getBytes());
        body.close();
    }
}

效果

Controller

@RestController
public class IndexController {

    @RequestMapping("/dem/{age}")
    public Demo demo(@PathVariable String age){
        System.out.println(age);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");
        return d;
    }

}

访问

http://localhost:8081/dem/1?format=json

image-20231017114843567

http://localhost:8081/dem/1?format=xml

image-20231017114908089

http://localhost:8081/dem/1?format=demo

可以看到这里与消息转换器写出方法的格式一致,证明成功

image-20231017144103939

修改路径参数format

将以下路径中的format改为自定义字符串

http://localhost:8081/dem/1?format=json

只需要两步:

  1. 修改配置类
  2. 写自定义策略

修改配置类

改造上面的配置类

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 内容协商相关配置
     * */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer)  {
		//自定义策略,默认的策略不会让自定义路径参数生效
        CustomContentNegotiationStrategy customStrategy = new CustomContentNegotiationStrategy();
        //parameterName 配置路径参数
        //strategies 设置自定义策略
        configurer.favorParameter(true).parameterName("type").strategies(Arrays.asList(customStrategy));
        
    }


    //添加自定义消息转换器
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MessageConverterForDemo());
    }

}

写自定义策略

默认的策略不会让自定义路径参数生效

public class CustomContentNegotiationStrategy implements ContentNegotiationStrategy {

    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        String type = request.getParameter("type");
        if ("json".equals(type)) {
            return Collections.singletonList(MediaType.APPLICATION_JSON);
        } else if ("xml".equals(type)) {
            return Collections.singletonList(MediaType.APPLICATION_XML);
        } else if("demo".equals(type)){
            return Collections.singletonList(MediaType.parseMediaType("application/x-demo"));
        }else{
            return null;
        }
    }
}

效果

Controller

@RestController
public class IndexController {

    @RequestMapping("/dem/{age}")
    public Demo demo(@PathVariable String age){
        System.out.println(age);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");
        return d;
    }

}

访问

http://localhost:8081/dem/1?type=xml

image-20231017155245084

http://localhost:8081/dem/1?type=json

image-20231017155302966

http://localhost:8081/dem/1?type=demo

image-20231017155322212

扩展名实现内容协商

不用路径参数format,而是直接通过请求路径的后缀实现

配置类

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 内容协商相关配置
     * */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer)  {
		
        //自定义协商策略
        CustomContentNegotiationStrategy customStrategy = new CustomContentNegotiationStrategy();

        Map<String, MediaType> mediaTypes = new HashMap<>();
        mediaTypes.put("demo", MediaType.parseMediaType("application/x-demo"));
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        mediaTypes.put("xml", MediaType.APPLICATION_XML);
		//Collections.singletonMap("xml", MediaType.APPLICATION_JSON); 将单个键值对变为map
        
        //添加媒体类型
        configurer.mediaTypes(mediaTypes)
//                 也可以不用map,用下面的方式分开添加
//                .mediaType("demo", MediaType.parseMediaType("application/x-demo"))
//                .mediaType("json", MediaType.APPLICATION_JSON)
//                .mediaType("xml", MediaType.APPLICATION_XML)
                .strategies(Arrays.asList(customStrategy));
    }


    //添加自定义消息转换器
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MessageConverterForDemo());
    }
}

自定义策略

从请求url截取后缀进行判断

import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;

public class CustomContentNegotiationStrategy implements ContentNegotiationStrategy {

    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) {

        String type = getFileExtension(request.getNativeRequest(HttpServletRequest.class).getRequestURI());

        if ("json".equals(type)) {
            return Collections.singletonList(MediaType.APPLICATION_JSON);
        } else if ("xml".equals(type)) {
            return Collections.singletonList(MediaType.APPLICATION_XML);
        } else if("demo".equals(type)){
            return Collections.singletonList(MediaType.parseMediaType("application/x-demo"));
        }else{
            return null;
        }
    }
    
	//截取方法
    private String getFileExtension(String url) {
        int lastDotIndex = url.lastIndexOf('.');
        if (lastDotIndex != -1) {
            return url.substring(lastDotIndex + 1);
        }
        return "";
    }
}

效果

Controller

@RestController
public class IndexController {

    @RequestMapping("/dem/{age}")
    public Demo demo(@PathVariable String age){
        System.out.println(age);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");
        return d;
    }

}

访问

http://localhost:8081/dem/1.json

image-20231017162259069

http://localhost:8081/dem/1.xml

image-20231017162342219

http://localhost:8081/dem/1.demo

image-20231017162411478

忽略请求头Accept

即不使用请求头Accept进行内容协商

如果忽略请求头中的Accept字段,ContentNegotiation将无法根据Accept字段来确定客户端期望的媒体类型。在这种情况下,要设置默认的媒体类型。因此,在使用ignoreAcceptHeader()方法时,请确保已根据自己的需求进行适当的配置和处理。

配置

@Configuration
public class webConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer)  {

        configurer
        //忽略请求头Accept
        .ignoreAcceptHeader(true)
        //设置默认返回媒体类型
        .defaultContentType(MediaType.APPLICATION_JSON);
        //.defaultContentTypeStrategy(.....) 设置默认内容协商策略

    }

4.异步处理请求

mvc的异步配置,为了节约Servlet容器(比如tomcat)的线程资源

在典型的Spring Web应用程序中,当一个HTTP请求到达服务器时,服务器(如Tomcat)将从其线程池中选取一个线程来处理这个请求。这个线程将负责执行整个请求的处理流程,包括调用Spring的Controller方法、进行业务逻辑处理、访问数据库等。在此过程中,Spring并不进行任何线程管理或调度。

因此,在不进行任何线程配置的情况下,Spring应用程序的并发处理能力主要取决于运行该应用程序的服务器的线程池配置,包括线程池的大小、线程调度策略等。

如果需要spring使用多线程,则需要我们进行配置。下面展示spring mvc中的异步配置及其实现的功能

使用线程池实现mvc异步

实现效果: Controller异步执行,与Service代码分别由不同线程执行,Controller不等待Service

配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class webConfig implements WebMvcConfigurer {
	
    /**
     * 开启请求异步处理
     * */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 设定异步请求线程池
        configurer.setTaskExecutor(asyncExecutor());
    }

    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }

}

Controller

import com.example.bean.Demo;
import com.example.service.TtlTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Callable;

@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;
    
    @GetMapping("test1")
    public Callable<Demo> test1() {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
		
        //异步执行代码
        Callable<Demo> callable = new Callable<Demo>() {
            @Override
            public Demo call() throws Exception {

                System.out.println("Call方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
				
                //Service代码
                Demo d = ttlTool.ttl3();

                System.out.println("Call方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                System.out.println();

                return d;
            }
        };

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return callable;
    }
    
}

Service

import org.springframework.stereotype.Component;
import com.example.bean.Demo;
import java.text.SimpleDateFormat;
import java.util.Date;


@Component
public class TtlTool {

    public Demo ttl3() throws InterruptedException {

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));


        Thread.sleep(5000);

        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));


        return d;
    }

}

效果

注意一个问题:在开启了Controller异步后,在某些浏览器上进行该Controller在同一时间的多次请求时,可能还是顺序执行,没有达到异步效果,这不是代码问题,而是浏览器原因,可以使用postman或多个不同浏览器测试

可以看到Controllertomcat的线程执行,ttl3()WebMvcConfigurer配置的线程池执行,实现了mvc异步配置

Controller方法被  http-nio-8081-exec-1  开始执行于 2023-10-20 16:58:09
Controller方法被  http-nio-8081-exec-1  结束执行于 2023-10-20 16:58:09
Call方法被  WebmvcThread-1  开始执行于 2023-10-20 16:58:09
异步方法被  WebmvcThread-1  开始执行于 2023-10-20 16:58:09
异步方法被  WebmvcThread-1  结束执行于2023-10-20 16:58:14
Call方法被  WebmvcThread-1  结束执行于 2023-10-20 16:58:14

前端返回

image-20231020165916999

异步超时设置

设置异步请求的超时时间。AsyncSupportConfigurersetDefaultTimeout 方法。

当处理异步请求时,如果没有通过特定的方式设置超时时间,就会使用默认的超时时间。如果超过了设置的超时时间,Spring MVC将取消异步处理,并返回超时结果给客户端。

配置类

通过超时拦截器实现

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.Callable;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 开启请求异步处理
     * */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 设定异步请求线程池
        configurer.setTaskExecutor(asyncExecutor());
        // 设置默认超时时间为5000毫秒,如果有其他方式设定的超时时间,这里会被覆盖
        configurer.setDefaultTimeout(3000);
        //超时拦截器,设置了达到超时时间后返回的结果
        configurer.registerCallableInterceptors(callableProcessingInterceptor());

    }

    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }

    //超时拦截器
    public CallableProcessingInterceptor callableProcessingInterceptor(){
        return new CallableProcessingInterceptor(){
            public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task){
                //这里可以做一些超时处理,比如记录超时日志
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                return "请求"+url+"已超时";
            }
        };
    }

}

yaml(可选)

可替换上面配置类中的setDefaultTimeout(注意不是替换全部,只替换超时时间配置),一样可以触发拦截器的处理

spring:
  mvc:
    async:
      request-timeout: 3000

Controller

import com.example.bean.Demo;
import com.example.service.TtlTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Callable;

@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;
    
    @GetMapping("test1")
    public Callable<Demo> test1() {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        Callable<Demo> callable = new Callable<Demo>() {
            @Override
            public Demo call() throws Exception {

                System.out.println("Call方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

                Demo d = ttlTool.ttl3();

                System.out.println("Call方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                System.out.println();

                return d;
            }
        };

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return callable;
    }
    
}

Service

import org.springframework.stereotype.Component;
import com.example.bean.Demo;
import java.text.SimpleDateFormat;
import java.util.Date;


@Component
public class TtlTool {

    public Demo ttl3() throws InterruptedException {

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

		//设置5秒模拟超时
        Thread.sleep(5000);

        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));


        return d;
    }

}

效果

可以看到与上面异步线程池结果不同的是,Call方法异步方法并没有输出结束执行,他们被mvc取消执行了,然后前端返回设置的超时结果

Controller方法被  http-nio-8081-exec-18  开始执行于 2023-10-21 09:16:34
Controller方法被  http-nio-8081-exec-18  结束执行于 2023-10-21 09:16:34
Call方法被  WebmvcThread-3  开始执行于 2023-10-21 09:16:34
异步方法被  WebmvcThread-3  开始执行于 2023-10-21 09:16:34

前端页面:

image-20231021091809357

Callable异步拦截器

拦截使用Callable执行的异步请求,对其做各种处理:

  • handleTimeout:当异步请求的处理时间超过设置的超时时间,就会执行此方法
  • handleError:当异步请求的处理过程中遇到异常时,就会执行此方法
  • beforeConcurrentHandling:在异步请求处理开始之前被调用,用于执行一些预处理操作
  • preProcess:在异步处理开始之前,beforeConcurrentHandling方法之后被运行
  • postProcess:在Callable异步执行结束后调用,对Callable异步任务结果进行后处理
  • afterCompletion:当异步处理完成时,不论是由于超时还是网络错误,都会从容器线程调用此方法

配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Callable;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 开启请求异步处理
     * */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 设定异步请求线程池
        configurer.setTaskExecutor(asyncExecutor());
        // 设置默认超时时间为5000毫秒,如果有其他方式设定的超时时间,这里会被覆盖
        configurer.setDefaultTimeout(5000);
        //超时拦截器,设置了达到超时时间后返回的结果
        configurer.registerCallableInterceptors(callableProcessingInterceptor());

    }

    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }

    //异步拦截器
    public CallableProcessingInterceptor callableProcessingInterceptor(){

        return new CallableProcessingInterceptor(){

            /**
             * 当异步请求的处理时间超过设置的超时时间,就会执行此方法
             * */
            public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task){
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                return "请求"+url+"已超时";
            }

            /**
             * 当异步请求的处理过程中遇到异常时,就会执行此方法
             * */
            public <T> Object handleError(NativeWebRequest request, Callable<T> task, Throwable throwable) {
                // 处理错误
                System.out.println("An error occurred during async processing: " + throwable.getMessage());
                // 进行错误处理逻辑,例如返回自定义错误页面或异常信息
                return "请求错误,出错信息: "+throwable.getMessage();
            }

            /**
             * 在异步请求处理之前,也就是在异步处理开始之前被调用。
             * 它可以用于在异步处理之前执行一些预处理操作,例如记录日志、设置上下文信息等
             * */
            public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("请求"+url+"的beforeConcurrentHandling方法执行了");
            }

            public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("请求"+url+"的preProcess方法执行了");
            }

            /**
             * 后置方法,对Callable处理结果进行后处理,在Callable异步执行结束后自动调用
             * @param concurrentResult Callable的返回结果
             * @param request http请求
             *
             * */
            public <T> void postProcess(NativeWebRequest request, Callable<T> task,
                                         Object concurrentResult)  {
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("postProcess输出: 请求"+url+"的异步执行结果是"+concurrentResult);
            }

            /**
             * 当异步处理完成时,不论是由于超时还是网络错误,都会从容器线程调用此方法
             *
             * */
            public <T> void afterCompletion(NativeWebRequest request, Callable<T> task){
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println();
                System.out.println("afterCompletion输出: 请求"+url+"已被线程"+Thread.currentThread().getName()+" 处理,并结束于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            }
        };
    }

}

Controller

import com.example.bean.Demo;
import com.example.service.TtlTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Callable;

@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;

    @GetMapping("test1")
    public Callable<Demo> test1() {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        Callable<Demo> callable = new Callable<Demo>() {
            @Override
            public Demo call() throws Exception {

                System.out.println("Call方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

                Demo d = ttlTool.ttl3();

                System.out.println("Call方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                System.out.println();

                return d;
            }
        };

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return callable;
    }

}

Service

import com.example.bean.Demo;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;

@Component
public class TtlTool {

    public Demo ttl3() throws InterruptedException {

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));


        Thread.sleep(3000);

        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));


        return d;
    }
}

结果

Controller方法被  http-nio-8081-exec-1  开始执行于 2023-10-21 11:41:15
Controller方法被  http-nio-8081-exec-1  结束执行于 2023-10-21 11:41:15

请求/test1的beforeConcurrentHandling方法执行了
请求/test1的preProcess方法执行了

Call方法被  WebmvcThread-1  开始执行于 2023-10-21 11:41:15
异步方法被  WebmvcThread-1  开始执行于 2023-10-21 11:41:15
异步方法被  WebmvcThread-1  结束执行于2023-10-21 11:41:18
Call方法被  WebmvcThread-1  结束执行于 2023-10-21 11:41:18

postProcess输出: 请求/test1的异步执行结果是Demo{one='1', two='2', three='3', four='4'}

afterCompletion输出: 请求/test1已被线程http-nio-8081-exec-5 处理,并结束于 2023-10-21 11:41:18

页面显示:

image-20231021114415410

DeferredResult异步拦截器

DeferredResult用于Controller异步回调,可以将异步执行的业务结果封装到其中,由Controller进行响应

拦截使用DeferredResult的异步请求,对其做各种处理:

  • handleTimeout:当异步请求的处理时间超过设置的超时时间,就会执行此方法,会被DeferredResult的超时覆盖
  • handleError:当异步请求的处理过程中遇到异常时,就会执行此方法
  • beforeConcurrentHandling:在异步请求处理开始后,DeferredResult设置前执行,用于执行一些预处理操作
  • preProcess:在上面的beforeConcurrentHandling方法之后,下面postProcess方法之前执行
  • postProcess:对DeferredResult设置异步任务结果进行后处理,在DeferredResult设置完成后调用
  • afterCompletion:当异步处理完成时,不论是由于超时还是网络错误,都会从容器线程调用此方法

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Callable;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 开启请求异步处理
     * */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 设定异步请求线程池
        configurer.setTaskExecutor(asyncExecutor());
        
        // 设置默认超时时间为5000毫秒,
        //如果有其他方式设定的超时时间,这里会被覆盖,比如为DeferredResult设置了超时,这里就不会再生效
        //configurer.setDefaultTimeout(3000);
        
        //DeferredResult异步拦截器,拦截的是DeferredResult的执行过程
        configurer.registerDeferredResultInterceptors(deferredResultProcessingInterceptor());

    }

    @Bean
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }



    public DeferredResultProcessingInterceptor deferredResultProcessingInterceptor(){

        return new DeferredResultProcessingInterceptor(){

            /**
             * 当异步请求的处理时间超过设置的超时时间,就会执行此方法   当
             * 针对DeferredResult未设置的情况,如果DeferredResult构造方法中设置了超时,则此方法不会执行。此方法可配合AsyncSupportConfigurer的setDefaultTimeout方法使用
             * @return 如果应该继续处理,则返回true;如果不应调用其他拦截器,则返回false。
             * */
            public  <T> boolean handleTimeout(NativeWebRequest request, DeferredResult<T> deferredResult){
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("请求"+url+"已超时,"+"handleTimeout方法"+
                        "   被线程"+Thread.currentThread().getName()+"   执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                deferredResult.setErrorResult( "已超时");
                return false;
            }

            /**
             * 当在DeferredResult设置之前处理异步请求时发生错误时,会从容器线程调用此方法。
             * */
            public <T> boolean handleError(NativeWebRequest request, DeferredResult<T> deferredResult,
                                           Throwable t)  {
                // 处理错误
                System.out.println("An error occurred during async processing: " + t.getMessage());
                // 进行错误处理逻辑,例如返回自定义错误页面或异常信息
                System.out.println("请求错误,出错信息: "+t.getMessage()+"   线程是"+Thread.currentThread().getName());
                return true;
            }

            /**
             * 在异步请求处理开始后,DeferredResult设置前执行,执行线程还是Controller的线程。
             * 它可以用于在DeferredResult设置之前执行一些预处理操作
             * */
            public <T> void beforeConcurrentHandling(NativeWebRequest request, DeferredResult<T> deferredResult) {
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("请求"+url+"的beforeConcurrentHandling方法"+
                        "   被线程"+Thread.currentThread().getName()+"   执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            }

            /**
             * 在beforeConcurrentHandling方法之后,postProcess方法之前执行,执行线程还是Controller的线程。
             * */
            public <T> void preProcess(NativeWebRequest request, DeferredResult<T> deferredResult) {
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("请求"+url+"的preProcess方法" +
                        "   被线程"+Thread.currentThread().getName()+"执行于   "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                System.out.println();
            }

            /**
             * 后置方法,对deferredResult处理结果进行后处理,在deferredResult异步执行结束后调用
             * 执行线程是执行deferredResult.setResult的线程
             * @param concurrentResult deferredResult的返回结果
             * @param request http请求
             *
             * */
            public <T> void postProcess(NativeWebRequest request, DeferredResult<T> deferredResult,
                                        Object concurrentResult)  {
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println("请求"+url+"的postProcess方法" +
                        "   被线程"+Thread.currentThread().getName()+"执行于   "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                        +",结果是"+concurrentResult);
            }

            /**
             * 当异步处理完成时,不论是由于超时还是网络错误,都会从容器线程调用此方法
             * */
            public <T> void afterCompletion(NativeWebRequest request, DeferredResult<T> deferredResult){
                HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
                String url = httpRequest.getRequestURI();
                System.out.println();
                System.out.println("请求"+url+"的afterCompletion方法" +
                        "   被线程"+Thread.currentThread().getName()+"执行于   "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            }
        };
    }

}

Controller

import com.example.bean.Demo;
import com.example.service.TtlTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.text.SimpleDateFormat;
import java.util.Date;


@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;
    
    @Autowired
    private ThreadPoolTaskExecutor asyncExecutor;


    @GetMapping("test2")
    public DeferredResult<Demo> test2() {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        //设置一个DeferredResult,并设置超时时间,如果下面的异步任务执行时间超过4秒,就返回已超时
        DeferredResult<Demo> deferredResult = new DeferredResult<>(4000l,"已超时");

        // 使用配置类中的线程池执行异步任务
        asyncExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("run方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                //这里模拟异步任务耗时操作
                Demo d = ttlTool.ttl3();
                //设置返回结果
                deferredResult.setResult(d);
                System.out.println("run方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            }
        });


        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return deferredResult;
    }

}

Service

import com.example.bean.Demo;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;


@Component
public class TtlTool {

    public Demo ttl3() {

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        return d;
    }
}

结果

Controller方法被  http-nio-8081-exec-1  开始执行于 2023-10-23 10:44:12
Controller方法被  http-nio-8081-exec-1  结束执行于 2023-10-23 10:44:12

run方法被  WebmvcThread-1  开始执行于 2023-10-23 10:44:12

异步方法被  WebmvcThread-1  开始执行于 2023-10-23 10:44:12

请求/test2的beforeConcurrentHandling方法   被线程http-nio-8081-exec-1   执行于2023-10-23 10:44:12
请求/test2的preProcess方法   被线程http-nio-8081-exec-1执行于   2023-10-23 10:44:12

异步方法被  WebmvcThread-1  结束执行于2023-10-23 10:44:15

请求/test2的postProcess方法   被线程WebmvcThread-1执行于   2023-10-23 10:44:15,结果是Demo{one='1', two='2', three='3', four='4'}

run方法被  WebmvcThread-1  结束执行于2023-10-23 10:44:15

请求/test2的afterCompletion方法   被线程http-nio-8081-exec-6执行于   2023-10-23 10:44:15

5.格式化器

在Spring MVC中,格式化器用于将请求参数转换为特定的数据类型,在处理请求时进行数据的格式化和解析。通常情况下,Spring MVC会自动为常见的数据类型提供默认的格式化器,例如日期、时间、数字等。

通过实现Formatter接口,重写其parseprint方法进行配置,其中:

  • parse:用于将String转为自定义类型
  • print:用于将自定义类型转为String
spring中自带一些Formatter格式化器的实现,也可以自己定义

@RequestParam注解为例,触发点在进行参数解析的数据绑定时:

AbstractNamedValueMethodArgumentResolver类的resolveArgument方法里:

调用WebDataBinderconvertIfNecessary方法进行数据转换时

image-20231024104219204

TypeConverterDelegate类中convertIfNecessary方法内this.propertyEditorRegistry包含mvc配置的Formatter

image-20231024104006080

配置类

import com.example.service.MyCustomDateFormatter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class webConfig implements WebMvcConfigurer {
    
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new DemoFormatter());
    }
}

自定义格式化器

import com.example.bean.Demo;
import org.springframework.format.Formatter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DemoFormatter implements Formatter<Demo> {
	
    /**
     * 在参数解析器解析请求参数时会被调用,
     * 目前测试中,对@RequestBody请求体携带的数据不会生效
     * */
    @Override
    public Demo parse(String text, Locale locale) throws ParseException {
        Demo myData = new Demo();
        if("1".equals(text)){
            System.out.println("parse执行");
            myData.setOne("1");
            myData.setTwo("2");
            myData.setThree("3");
            myData.setFour("4");
            SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd");
            Date newDate = s.parse(s.format(new Date()));
            myData.setDate(newDate);
        }
        return myData;
    }
	
    /**
     * 用于Object转String,除了手动进行调用外,暂时不知道在哪里生效
     * */
    @Override
    public String print(Demo object, Locale locale) {
        System.out.println("print执行");
        return object.toString();
    }
}

实体类

public class Demo {

    private String one;
    private String two;


    private String three;
    private String four;

    public Date getDate() {
        return date;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "one='" + one + '\'' +
                ", two='" + two + '\'' +
                ", three='" + three + '\'' +
                ", four='" + four + '\'' +
                ", date=" + date +
                '}';
    }

    public void setDate(Date date) {
        this.date = date;
    }

    //指定解析日期的格式,请求传入的日期必须是这个格式否则无法解析报400,返回给前端的格式也会是这个
    //@JsonFormat(pattern = "yyyy/MM/dd")
    private Date date;

    public String getOne() {
        return one;
    }

    public void setOne(String one) {
        this.one = one;
    }

    public String getTwo() {
        return two;
    }

    public void setTwo(String two) {
        this.two = two;
    }

    public String getThree() {
        return three;
    }

    public void setThree(String three) {
        this.three = three;
    }

    public String getFour() {
        return four;
    }

    public void setFour(String four) {
        this.four = four;
    }
}

Controller

@RestController
public class IndexController {

	@RequestMapping(value = "/test")
    public String test2(@RequestParam Demo d) {
        System.out.println("Controller: "+d.toString());
        return d.toString();
    }
    
}

效果

请求参数为1时,直接将1转为一个Demo类对象

image-20231024143805934

image-20231024143754475

时间格式化

返回json数据

yaml指定全局时间格式化

spring:
  #指定解析日期的格式,请求传入的日期必须是这个格式否则无法解析,返回给前端的格式也会是这个
  jackson:
    date-format: yyyy/MM/dd

在实体类字段中单独指定

//指定解析日期的格式,请求传入的日期必须是这个格式否则无法解析报400,返回给前端的格式也会是这个
@JsonFormat(pattern = "yyyy/MM/dd")
private Date date;

接收json数据

需要依赖

<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

普通Date

import org.springframework.format.annotation.DateTimeFormat;

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;

LocalDate

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDate exceptionTime;

LocalDateTime

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime exceptionTime;

6.拦截器

用于在请求执行前后和完成时进行拦截处理

源码:在 DispatcherServlet前端控制器 处理请求的doDispatch方法中,执行时机如下:

image-20231025085733556

Controller

@RestController
public class IndexController {
	
	@RequestMapping(value = "/test3")
    public Demo test3() {
        Demo myData = new Demo();
        System.out.println("parse执行");
        myData.setOne("1");
        myData.setTwo("2");
        myData.setThree("3");
        myData.setFour("4");
        myData.setDate(new Date());
        return myData;
    }
	
}

配置类

@Configuration
public class webConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry
                //添加一个拦截器
                .addInterceptor(new IndexInterceptor())
                //排除某些路径不被拦截,参数为字符串可变参
                .excludePathPatterns("/a")
                //排除某些路径不被拦截,参数为List
                .excludePathPatterns(Arrays.asList(new String[]{"/a","/b","/c"}))
                //添加路径,参数为字符串可变参
                .addPathPatterns("/test3")
                //添加路径,参数为List
                .addPathPatterns(Arrays.asList(new String[]{"/test1","/test2","/test3"}))
                //指定拦截器的优先级,用于设置拦截器的执行顺序,参数是一个整数值,值越小,优先级越高,默认值为0。
                .order(1);
                //用于设置路径匹配器,用于匹配拦截的路径模式。参数 pathMatcher 是一个实现了 PathMatcher 接口的路径匹配器对象。
                //默认情况下,Spring MVC使用的是 AntPathMatcher,可以根据需要进行自定义。
                //.pathMatcher()
    }

}

自定义拦截器

@Slf4j
public class IndexInterceptor implements HandlerInterceptor {


    /**
     * 执行目标方法之前
     * @param handler 执行链中的handle
     * @return true:向下执行下一个拦截器,没有拦截器就是执行mvc处理;  false:不向下执行,结束后续处理
     * */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURI();
        String accept = request.getHeader("Accept");
        System.out.println("preHandle执行---拦截的路径是: "+url+",请求头Accept内容为"+accept);
        /*
        实现一个根据session验证用户是否登录功能
        HttpSession session = request.getSession();
        Object user = session.getAttribute("user");
        if(user!=null){
        return true;
        }
        重定向方式
        session.setAttribute("msg","请先登录");
        response.sendRedirect("/login");
        转发方式
        request.setAttribute("msg","请先登录");
        request.getRequestDispatcher("/").forward(request,response);
         */
        return true;
    }


    /**
     * 在执行完目标Controller方法,并处理返回值之后(json数据不需要视图解析,处理完目标方法就响应返回了),视图解析之前
     * @param handler 执行链中的handle
     * */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod hm = (HandlerMethod) handler;
        String name = hm.getBean().getClass().getName();//目标controller类名
        System.out.println("postHandle执行---目标Controller的类名为: "+name);
        //获取控制器方法的返回值,modelAndView类型,无法获取json
        //Object returnValue = modelAndView.getModel().get("returnValue");
        //System.out.println("postHandle执行---控制器方法的返回值:" + returnValue);
    }

    /**
     * 在请求完成后执行的逻辑
     * 在DispatcherServlet前端控制器最后,调用processDispatchResult方法处理返回结果时会调用此方法
     * @param handler 执行链中的handle
     * */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        // 获取请求执行的结果
        int status = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse().getStatus();

        // 在这里进行处理结果的逻辑,不影响HTTP响应

        // 输出请求执行结果
        System.out.println("请求执行结果:" + status);
    }
}

效果

image-20231025084941472

image-20231025085010358

7.静态资源请求处理

Servlet容器的默认ServletDispatcherServlet对静态资源的处理有什么不同

  1. 映射路径:Servlet容器的默认Servlet通常会将静态资源的映射路径设置为"/*",这意味着它会处理所有的请求,包括静态资源请求。而DispatcherServlet通常会将自己的映射路径设置为"/",这意味着它会拦截所有的请求,包括静态资源请求。
  2. 处理逻辑 Servlet容器的默认Servlet会根据静态资源的请求路径直接返回对应的静态资源文件,而不经过任何Spring MVC的处理逻辑。这样可以快速地返回静态资源,提高性能。而DispatcherServlet会将静态资源的请求交给配置的资源处理器(如ResourceHttpRequestHandler)来处理,这一过程可能会涉及资源的查找和处理逻辑。
  3. 配置优先级:Servlet容器的默认Servlet的处理优先级较低,通常在其他Servlet或Filter无法处理请求时才会被调用。而DispatcherServlet的处理优先级较高,它是Spring MVC的核心组件,会拦截大部分的请求,包括静态资源请求。

综上所述,Servlet容器的默认ServletDispatcherServlet在处理静态资源方面有不同的映射路径、处理逻辑和配置优先级。通常情况下,可以通过配置WebMvcConfigurer中的configureDefaultServletHandling方法来决定是否将静态资源的处理交给Servlet容器的默认Servlet。

配置

WebMvcConfigurerconfigureDefaultServletHandling方法用于配置Spring MVC是否应该将静态资源请求转发给Servlet容器的默认Servlet来处理。

  • 当在Spring MVC中使用DispatcherServlet来处理请求时,默认情况下,静态资源(如CSS、JavaScript、图片等)的请求也会被DispatcherServlet拦截。然后,DispatcherServlet会根据配置的资源处理器来处理这些静态资源请求。

  • 但是,在某些情况下,我们可能希望将静态资源的请求直接交给Servlet容器的默认Servlet来处理,而不经过DispatcherServlet
    这样可以提高静态资源的处理效率,并减轻DispatcherServlet的负担。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class webConfig implements WebMvcConfigurer {
    
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable(); // 启用默认Servlet处理静态资源请求
    }

}

8.静态资源路径匹配

DispatcherServlet前端控制器处理请求的doDispatch方法中如下代码执行静态资源处理:

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

也就是适配器里执行

配置

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 用于配置静态资源处理器,即用于处理静态资源(如图片、CSS、JS等文件)的请求。
     * */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
                //指定匹配静态资源的请求前缀,
                //**是静态资源的位置,比如classpath:/static/文件夹下还有多级文件夹,访问路径后面也要加上文件路径
                .addResourceHandler("/static/**")
                //指定静态资源文件的位置
                .addResourceLocations("classpath:/static/");

        //开启静态资源处理链,自定义资源解析器与资源转换器,以实现更细粒度的控制和操作
        //.resourceChain(true)
        //.addResolver(new MyResourceResolver()) // 添加自定义资源解析器
        //.addTransformer(new MyResourceTransformer()) // 添加自定义资源转换器
        //.addTransformer(new MyResourceTransformer2()); // 添加更多的自定义资源转换器

        //用于检查是否存在参数路径的静态资源处理,上面设置了addResourceHandler("/static/**"),这里就会返回true
        registry.hasMappingForPattern("/static/**");


    }

}

yaml

与上面配置类任选其一使用即可

server:
  port: 8080

spring:
  #静态资源访问前缀
  mvc:
    static-path-pattern: /static/**
  #静态资源在项目中的存放路径
  web:
    resources:
      static-locations:
        [ classpath:/static/ ]

项目静态资源位置

image-20231102114914552

访问结果

http://localhost:8081/static/泰山夜景.jpg

image-20231102114737515

http://localhost:8081/static/photos/泰山夜景1.jpg

image-20231102114839511

9.跨域请求处理

配置

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 出于浏览器的同源策略限制,协议、域名、端口号任一不同即为跨域
     *
     * 这里我们的CORSConfiguration配置类继承了WebMvcConfigurer父类并且重写了addCorsMappings方法,我们来简单介绍下我们的配置信息
     *  allowedOrigins:允许设置的请求域名访问我们的跨域资源,可以固定单条或者多条内容,如:"http://www.baidu.com",只有百度可以访问我们的跨域资源。
     *  addMapping:配置可以被跨域的路径,可以任意配置,可以具体到直接请求路径。
     *  allowedMethods:设置允许的请求方法类型访问该跨域资源服务器,如:POST、GET、PUT、OPTIONS、DELETE等。
     *  allowedHeaders:允许所有的请求header访问,可以自定义设置任意请求头信息,如:"X-YYYY-TOKEN"
     *  allowCredentials: 是否允许请求带有验证信息,用户是否可以发送、处理 cookie
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")//项目中的所有接口都支持跨域
                .allowedOrigins("*")//所有地址都可以访问,也可以配置具体地址, 注意必须提供完整的地址,不支持通配符或模式匹配
                .allowCredentials(true) //是否允许请求带有验证信息
                .allowedMethods("*")//"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"
                .allowedHeaders("*")//允许所有的请求header访问
                //.allowedOriginPatterns() //同allowedOrigins(""),不过可以使用通配符或模式匹配
                //.combine() //用于将当前的CorsRegistry与另一个CorsRegistry进行合并
                //.exposedHeaders() //用于设置在响应中暴露给客户端的自定义响应头。参数是一个字符串数组,可以指定多个响应头。这些响应头会被添加到响应的Access-Control-Expose-Headers头中,允许客户端访问这些响应头。
                .maxAge(3600);// 跨域允许时间
    }

}

10.配置视图控制器

用于配置简单的视图控制器(View Controller)。

视图控制器是用于将请求映射到具体视图的一种简化方式。通过配置视图控制器,可以直接将某个URL路径映射到指定的视图,而无需编写Controller方法。

作用:

  • 用于配置简单的视图控制器,将请求映射到具体的视图。

配置

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 视图控制器是用于将请求映射到具体视图的一种简化方式。
     * 通过配置视图控制器,可以直接将某个URL路径映射到指定的视图,而无需编写Controller方法。
     * */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        //为"/login"请求返回登陆页面,无需写controller处理"/login"
        registry.addViewController("/login").setViewName("login");
        
		/**
			下面是视图控制器的其他功能应用
		*/       
        registry
                //添加重定向视图控制前,将指定路径重定向到另一个路径
                .addRedirectViewController("/red","/redict")
                //设置重定向的URL是否相对于上下文路径。如果设置为true,则重定向的URL将相对于上下文路径,否则将是绝对路径。
                .setContextRelative(true)
                //设置是否保留原始请求的查询参数. 如果设置为true,则保留原始请求的查询参数,添加到重定向的URL中。
                .setKeepQueryParams(true)
                //用于设置重定向的HTTP状态码。参数是一个HttpStatus枚举值,表示要设置的HTTP状态码。
                //默认情况下,重定向的HTTP状态码为302 Found。
                .setStatusCode(HttpStatus.FOUND);

        
        //将 "/status" 请求路径映射到状态码为404的HTTP响应。这样,当请求/status时,会返回一个404 Not Found的HTTP响应。
        registry.addStatusController("/status", HttpStatus.NOT_FOUND);
    }


效果

http://localhost:8081/login

image-20231106083847393

11.配置视图解析器

视图解析器用于将逻辑视图名称解析为具体的视图对象,并进行渲染。通过配置视图解析器,可以定义视图的查找规则、视图文件的位置、视图的解析方式等。

下面写一个简单的自定义视图解析器

自定义视图

自定义一个视图,作为后面自定义视图解析器的解析对象

public class MyCustomView implements View {

    @Override
    public String getContentType() {
        // 返回视图的内容类型
        return "text/html";
    }

    /**
     * 视图渲染方法,渲染到前端
     * */
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 自定义视图渲染逻辑
        PrintWriter writer = response.getWriter();
        writer.println("<h1>Hello, Custom View!</h1>");
        writer.flush();
    }
}

自定义视图解析器

通过resolveViewName解析视图名,获取一个支持解析目标视图的解析器

import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import java.util.Locale;


public class MyViewResolver implements ViewResolver {

    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        if (viewName.startsWith("custom:")) {
            // 自定义逻辑,根据视图名称返回自定义的视图对象
            return new MyCustomView();
        }
        // 如果无法处理该视图名称,返回null
        return null;
    }
}

配置

import com.example.view.MyViewResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     *  添加自定义视图解析器
     * */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(new MyViewResolver());

    }

}

效果

controller访问

@RestController
public class IndexController {
    
    @GetMapping("/custom-view")
    public String customView() {
        return "custom:myCustomView";
    }
    
}
http://localhost:8081/custom-view

image-20231025131803823

可见返回的是自定义视图中的html内容

在这个示例中,我们在MyController中定义了一个处理/custom-view请求的方法,并在方法中返回"custom:myCustomView"作为逻辑视图名称。通过前缀"custom:"来标识这是一个自定义视图。

当请求"/custom-view"时,Spring MVC将会根据逻辑视图名称"custom:myCustomView"来解析对应的视图。由于我们配置了自定义的视图解析器,它将会调用我们自定义视图解析器的resolveViewName方法,并根据逻辑视图名称"custom:myCustomView"返回我们的自定义视图对象MyCustomView

12.配置参数解析器

方法参数解析器用于将请求中的数据解析为Controller方法的参数。通过配置方法参数解析器,可以扩展Spring MVC的默认解析逻辑,以支持更多自定义的参数类型或解析规则。

查询参数解析

实现请求参数由字符串转日期

首先,如果按如下地址对controller进行请求,会报错400:

地址

http://localhost:8081/date?d=2023-10-25

Controller

下面的代码要加@DateTimeFormat,否则可能会解析失败

@RequestMapping("/date")
public String date(@RequestParam  Date d) {
	System.out.println(d.toString());
	return d.toString();
}

原因是因为在请求参数d的注解中使用了@RequestParam来绑定Date类型的参数。然而,Spring MVC默认使用String类型进行请求参数的绑定。要在参数添加@DateTimeFormat(pattern = "yyyy-MM-dd")才可解决:

@RequestMapping("/date")
public String date(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date d) {
	System.out.println(d.toString());
	return d.toString();
}

接下来,不使用@DateTimeFormat,使用自定义注解@CustomDateParam来解决上面问题

Controller

@RestController
public class IndexController {

	@RequestMapping("/date")
    public String date(@CustomDateParam Date d) {
        System.out.println(d.toString());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(d);
    }

}

接下来看具体配置

自定义请求参数注解

import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.ValueConstants;
import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomDateParam {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default ValueConstants.DEFAULT_NONE;

}

自定义参数解析器

因为spring从http请求中拿到的参数都是默认为String类型的,这里把她转成Date类型

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.multipart.support.MultipartResolutionDelegate;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CustomArgumentResolver  extends AbstractNamedValueMethodArgumentResolver 
//implements HandlerMethodArgumentResolver
{

    private final Map<MethodParameter, AbstractNamedValueMethodArgumentResolver.NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);

	//通过此方法支持CustomDateParam注解
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(CustomDateParam.class)){
            return true;
        }else{
            return false;
        }
    }

    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        CustomDateParam ann = parameter.getParameterAnnotation(CustomDateParam.class);
        return (ann != null ? new CustomParamNamedValueInfo(ann) : new CustomParamNamedValueInfo());
    }

    @Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

        if (servletRequest != null) {
            Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
            if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
                return mpArg;
            }
        }

        Object arg = null;
        MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
        if (multipartRequest != null) {
            List<MultipartFile> files = multipartRequest.getFiles(name);
            if (!files.isEmpty()) {
                arg = (files.size() == 1 ? files.get(0) : files);
            }
        }
        if (arg == null) {
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
            }
        }

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date result = sdf.parse(String.valueOf(arg));

        return result;
    }


    private static class CustomParamNamedValueInfo extends NamedValueInfo {

        public CustomParamNamedValueInfo() {
            super("", false, ValueConstants.DEFAULT_NONE);
        }

        public CustomParamNamedValueInfo(CustomDateParam annotation) {
            super(annotation.name(), annotation.required(), annotation.defaultValue());
        }
    }

}

配置类

注册自定义参数解析器到mvc中

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.*;
import java.util.List;

@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     *  添加自定义参数解析器
     * */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {

        resolvers.add(new CustomArgumentResolver());

    }

}

验证

再次访问:

http://localhost:8081/date?d=2023-10-25

前端显示如下,且不报错:

image-20231025153954443

请求体参数解析

实现请求体参数数字转字符串

{
	"one": "1",
	"two": "2",
	"three": "3",
	"four": "4",
	"date": "2023-10-25"
}

转为

{
    "one": "一",
    "two": "二",
    "three": "三",
    "four": "四",
    "date": "2023-10-25"
}

自定义请求参数注解

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomBody {

	//为true则表示必须被配置
	//举个例子,如果一个类有一个名为name的属性,使用了@Required注解,那么在配置这个类的bean时,必须在配置文件中设置name属性的值,否则容器就会抛出异常
	boolean required() default true;

}

自定义参数解析器

import com.example.bean.Demo;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;


/**
 * 由于需要解析的请求数据是json,所以要继承可以转换json数据的消息转换器,来实现自定义参数解析器
 * */
public class CustomBodyArgumentResolver  extends AbstractMessageConverterMethodProcessor {


    protected CustomBodyArgumentResolver(List<HttpMessageConverter<?>> converters) {
        super(converters);
    }

    /**
     *  判断是否支持解析@CustomBody自定义注解
     * */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CustomBody.class);
    }

    /**
     *  解析请求参数为Demo类
     * */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        parameter = parameter.nestedIfOptional();
        //调用消息转换器转换消息为具体类型
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }

        return adaptArgumentIfNecessary(arg, parameter);
    }

    @Override
    protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
                                                   Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        Assert.state(servletRequest != null, "No HttpServletRequest");
        ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
        //将请求中的数据转换为具体的参数类型
        Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
        if (arg == null && checkRequired(parameter)) {
            throw new HttpMessageNotReadableException("Required request body is missing: " +
                    parameter.getExecutable().toGenericString(), inputMessage);
        }

        Demo demo = null;
        
        if (paramType instanceof Class) {

            Class<?> clazz = (Class<?>) paramType;
            if (clazz.equals(Demo.class)) {
                demo  = (Demo) arg;
                demo.setOne(convertToChineseNumber(demo.getOne()));
                demo.setTwo(convertToChineseNumber(demo.getTwo()));
                demo.setThree(convertToChineseNumber(demo.getThree()));
                demo.setFour(convertToChineseNumber(demo.getFour()));
                return demo;
            }
        }

        return arg;
    }
    
    /**
     * 数字文字转换方法
     * */
    public String convertToChineseNumber(String numberString) {
        String[] chineseNumbers = {"一", "二", "三", "四", "五", "六", "七", "八", "九"};
        int number = Integer.parseInt(numberString);

        if (number >= 1 && number <= 9) {
            return chineseNumbers[number - 1];
        } else {
            throw new IllegalArgumentException("Invalid number: " + numberString);
        }
    }

    protected boolean checkRequired(MethodParameter parameter) {
        CustomBody requestBody = parameter.getParameterAnnotation(CustomBody.class);
        return (requestBody != null && requestBody.required() && !parameter.isOptional());
    }
    
    
    
    /**
     * 判断是否支持返回值的,这里解析请求参数暂时不用实现
     * */
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return true;
    }

    /**
     * 解析返回值的,这里解析请求参数暂时不用实现
     * */
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

    }

}

配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.*;
import java.util.ArrayList;
import java.util.List;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     *  添加自定义参数解析器
     * */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {

        List<HttpMessageConverter<?>> converters = new ArrayList<>();
        //注入json消息转换器
        converters.add(new MappingJackson2HttpMessageConverter());
		
        //添加自定义请求体参数解析器
        resolvers.add(new CustomBodyArgumentResolver(converters));
    }

}

验证

Controller

@RestController
public class IndexController {
	@RequestMapping("/body")
    public Demo test3(@CustomResponseBody Demo d) {
        System.out.println(d.toString());
        return d;
    }
}

访问

http://localhost:8081/body

数据为

{
	"one": "1",
	"two": "2",
	"three": "3",
	"four": "4",
	"date": "2023-10-25"
}

结果:

image-20231026100115283

13.配置返回值处理器

实现请求返回值数字转字符串

{
	"one": "1",
	"two": "2",
	"three": "3",
	"four": "4",
	"date": "2023-10-25"
}

转为

{
    "one": "一",
    "two": "二",
    "three": "三",
    "four": "四",
    "date": "2023-10-25"
}

自定义注解

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


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomResponseBody {

}

自定义返回值处理器

import com.example.bean.Demo;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;


public class CustomBodyHandleReturn extends AbstractMessageConverterMethodProcessor {


    public CustomBodyHandleReturn(List<HttpMessageConverter<?>> converters) {
        super(converters);
    }


    /**
     * 判断是否支持返回值注解 @CustomResponseBody
     * */
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {

        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), CustomResponseBody.class) ||
                returnType.hasMethodAnnotation(CustomResponseBody.class));

    }


    /**
     * 解析返回值方法,调用了父类实现的消息转换器
     * */
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
        returnType.getParameterType();

        Demo demo = null;

        //这里实现数字与文字转换
        if(returnValue.getClass().isAssignableFrom(Demo.class)){

            demo  = (Demo) returnValue;
            demo.setOne(convertToChineseNumber(demo.getOne()));
            demo.setTwo(convertToChineseNumber(demo.getTwo()));
            demo.setThree(convertToChineseNumber(demo.getThree()));
            demo.setFour(convertToChineseNumber(demo.getFour()));

            writeWithMessageConverters(demo, returnType, inputMessage, outputMessage);

        }else{

            writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);

        }

    }


    /**
     * 数字文字转换方法
     * */
    public String convertToChineseNumber(String numberString) {
        String[] chineseNumbers = {"一", "二", "三", "四", "五", "六", "七", "八", "九"};
        int number = Integer.parseInt(numberString);

        if (number >= 1 && number <= 9) {
            return chineseNumbers[number - 1];
        } else {
            throw new IllegalArgumentException("Invalid number: " + numberString);
        }
    }
    
    /**
     * 判断是否支持参数解析的,这个示例是解析返回值的,所以暂时不用实现
     * */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return false;
    }

    /**
     *  解析请求参数,这个示例是解析返回值的,所以暂时不用实现
     * */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return null;
    }

    /**
     * 用消息转换器读请求参数,这个示例是解析返回值的,所以暂时不用实现
     * */
    @Override
    protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
                                                   Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        return null;
    }

}

配置类

import com.example.cuntom.CustomBodyHandleReturn;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.*;
import java.util.ArrayList;
import java.util.List;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     *  添加自定义返回值处理器
     * */
    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {

        List<HttpMessageConverter<?>> converters = new ArrayList<>();
        //注入json消息转换器

        converters.add(new MappingJackson2HttpMessageConverter());
		
        //添加返回值处理器
        handlers.add(new CustomBodyHandleReturn(converters));
    }

}

验证

Controller

注意加到类上,或者加到方法并去掉类上的@RestController@ResponseBody,否则会被覆盖失效

//@RestController
@Controller
@CustomResponseBody
public class IndexController {

	@RequestMapping("/body")
    public Demo test3(@RequestBody Demo d) {
        System.out.println(d.toString());
        return d;
    }
    
}    

访问

地址

http://localhost:8081/body

数据

{
	"one": "1",
	"two": "2",
	"three": "3",
	"four": "4",
	"date": "2023-10-25"
}

效果

image-20231026112240167

14.配置消息转换器

消息转换器MessageConverter)用于处理请求和响应中的数据,将其从一种表示形式转换为另一种表示形式。消息转换器在处理HTTP请求和响应时,负责将Java对象与HTTP消息(如请求体、响应体)之间进行转换,在转换为HTTP消息时,还可以指定json、xml等各种格式

自定义消息转换器

上面介绍 内容协商-自定义媒体类型 时已经有一个消息转换器的示例,这里在简单写一个,还是实现json串中的数字转为汉字,并应用在返回值处理中

import com.example.bean.Demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;


public class NumberToStringForDemo implements HttpMessageConverter<Demo> {
    
    /**
     * 本示例主要展示响应时的转换,请求暂不实现
     * */
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    /**
     * 判断消息转换器支持转换的java类
     * */
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {

        return clazz.isAssignableFrom(Demo.class);
    }

    /**
     * 指定当前消息转换器可以支持的媒体类型
     * */
    @Override
    public List<MediaType> getSupportedMediaTypes() {

        return MediaType.parseMediaTypes("application/json");
    }

    /**
     * read用于读取请求参数
     * 本示例主要展示响应时的转换,请求暂不实现
     * */
    @Override
    public Demo read(Class<? extends Demo> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    /**
     * 消息转换器的转换方法
     * */
    @Override
    public void write(Demo demo, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

        demo.setOne(convertToChineseNumber(demo.getOne()));
        demo.setTwo(convertToChineseNumber(demo.getTwo()));
        demo.setThree(convertToChineseNumber(demo.getThree()));
        demo.setFour(convertToChineseNumber(demo.getFour()));

        ObjectMapper objectMapper = new ObjectMapper();
        // 将Java对象转换为JSON字符串
        String json = objectMapper.writeValueAsString(demo);

        // 将JSON字符串反序列化为Java对象
        //Demo obj = objectMapper.readValue(json, Demo.class);

        //自定义数据格式的写出
        //String data = demo.getOne() + ";" + demo.getTwo() + ";" + demo.getThree()  + ";" + demo.getFour();

        //写出去
        OutputStream body = outputMessage.getBody();
        body.write(json.getBytes());
        body.close();
    }


    /**
     * 数字文字转换方法
     * */
    public String convertToChineseNumber(String numberString) {
        String[] chineseNumbers = {"一", "二", "三", "四", "五", "六", "七", "八", "九"};
        int number = Integer.parseInt(numberString);

        if (number >= 1 && number <= 9) {
            return chineseNumbers[number - 1];
        } else {
            throw new IllegalArgumentException("Invalid number: " + numberString);
        }
    }
}

配置类

import com.example.cuntom.NumberToStringForDemo;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.*;
import java.util.List;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     *  添加自定义消息转换器,会加到转换器集合的最后一个位置,
     *  遍历时最后一个获取,如果前面有能对当前请求进行转换的,这个就不会被使用了
     * */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //在自定义消息转换器前已经有了MappingJackson2HttpMessageConverter,他可以将json转为所有自定义java类,所以这里的自定义消息转换器不会生效
        //converters.add(new NumberToStringForDemo());
    }

    /**
     *  添加自定义消息转换器,可以指定优先级,也就是可以指定存放在转换器集合的任意位置。
     * */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //放到最前端
        converters.add(0,new NumberToStringForDemo());
    }

}

验证

Controller

@RestController
public class IndexController {

	RequestMapping("/body")
    public Demo test3(@RequestBody Demo d) {
        System.out.println(d.toString());
        return d;
    }

}

访问地址并填写请求参数

http://localhost:8081/body
{
	"one": "1",
    "two": "2",
    "three": "3",
    "four": "4",
    "date": "2023-10-25"
}

结果

image-20231026132803138

15.配置异常解析器

用于配置全局的异常解析器(HandlerExceptionResolver
异常解析器是用于处理Spring MVC应用程序中抛出的异常的组件。它将异常转换为HTTP响应的形式,以便客户端能够正确处理异常。

自定义异常

public class CustomException extends Exception {

    public CustomException(String message) {
        super(message);
    }
}

自定义异常解析器

返回一个异常视图给前端页面

import org.springframework.core.Ordered;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class CustomExceptionResolver implements HandlerExceptionResolver, Ordered {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex instanceof CustomException) {
            // 处理自定义异常
            CustomException customException = (CustomException) ex;
            String errorMessage = customException.getMessage();

            ModelAndView modelAndView = new ModelAndView();
            modelAndView.addObject("errorMessage", errorMessage);
            modelAndView.setViewName("error"); // 设置对应的错误页面视图名称

            return modelAndView;
        }

        // 如果不是自定义异常,则返回null,交给其他异常解析器处理
        return null;
    }

    @Override
    public int getOrder() {
        // 设置优先级,数字越小优先级越高
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

异常视图页面

导入依赖

<!-- Spring Boot Thymeleaf Starter -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf页面

${errorMessage}是异常响应信息

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
<h1>Hello,An Error</h1>
<p th:text="${errorMessage}"></p>
</body>
</html>

配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;

/**
* 用于配置全局的异常解析器(HandlerExceptionResolver)
* 异常解析器是用于处理Spring MVC应用程序中抛出的异常的组件。它将异常转换为HTTP响应的形式,以便客户端能够正确处理异常。
* */
@Configuration
public class webConfig implements WebMvcConfigurer {
	
    /**
     *	添加一个异常解析器,会放在异常解析器列表末尾
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new CustomExceptionResolver());
    }
    
    /**
     * 与上面不同的是,这个方法可以指定自定义异常解析器在解析器列表的位置
     * */
    @Override
    public  void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(0,new CustomExceptionResolver());
    }

}

验证

controller

@RestController
public class IndexController {

    @RequestMapping("ex")
    public void ex() throws CustomException {
        throw new CustomException("一个自定义异常");
    }
    
}

访问地址

http://localhost:8080/ex

效果:

image-20231026143232444

ControllerAdvice

另一种全局异常处理器方式,但只会处理Controller的,不配置mvc的异常解析器

自定义异常

public class CustomException extends Exception {

    public CustomException(String message) {
        super(message);
    }
}

异常视图页面

导入依赖

<!-- Spring Boot Thymeleaf Starter -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf页面

${errorMessage}是异常响应信息

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
<h1>Hello,An Error</h1>
<p th:text="${errorMessage}"></p>
</body>
</html>

Advice配置

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

/**
* 处理整个web controller的异常,会把返回值添加到response进行返回
* */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ModelAndView handlerArithException(Exception e){
        System.out.println("异常是:"+e.getMessage());

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("errorMessage", e.getMessage());
        modelAndView.setViewName("error"); // 设置对应的错误页面视图名称

        return modelAndView;
    }

}

Controller

@RestController
public class IndexController {
    
    @RequestMapping("ex")
    public void ex() throws CustomException {
        throw new CustomException("一个自定义异常");
    }
}

验证

最后结果与上面mvc配置一致

访问地址

http://localhost:8080/ex

效果:

image-20231026151259399

16.配置验证器

  • WebMvcConfigurerValidator作用是配置和自定义Spring MVC中的数据验证器(Validator)。
  • 通过实现WebMvcConfigurer接口并重写其中的getValidator()方法,可以将自定义的Validator应用于Spring MVC中的数据验证。
  • 在Spring MVC中,数据验证是通过使用javax.validation框架实现的。该框架提供了注解(如@NotNull、@Size等)来对数据进行验证。当在Controller中接收到请求参数时,Spring MVC会自动对这些参数进行验证,并将验证结果存储在BindingResult对象中。
  • WebMvcConfigurerValidator可以用于自定义验证逻辑,例如,对请求参数进行更复杂的验证或使用自定义的验证注解。通过实现WebMvcConfigurer接口并重写getValidator()方法,可以将自定义的Validator注册到Spring MVC中,以覆盖默认的验证行为。

配置类

import com.example.cuntom.CustomValidator;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.Validator;
import org.springframework.web.servlet.config.annotation.*;


@Configuration
public class webConfig implements WebMvcConfigurer {

    /**
     * 添加一个自定义验证器
     * */
    @Override
    public Validator getValidator() {
        return new CustomValidator();
    }

}

自定义验证器

import com.example.bean.Demo;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class CustomValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        // 指定支持验证的目标类,例如表单对象类
        return Demo.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Demo userForm = (Demo) target;

        // 使用ValidationUtils类提供的一些常用验证方法
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "one", "NotEmpty");

        // 自定义验证逻辑
        if (!"1".equals(userForm.getOne()) && !"一".equals(userForm.getOne())) {
            errors.rejectValue("one", "One.ValueError","Demo的one属性必须是1或一");
        }
    }
}

处理验证器出错的异常解析器

import java.util.List;
import java.util.stream.Collectors;

/**
 * 处理整个web controller的异常,会把返回值添加到response进行返回
 * */
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ModelAndView methodArgumentNotValidException(MethodArgumentNotValidException e){
        System.out.println("异常是:"+e.getMessage());

        List<ObjectError> allErrors = e.getAllErrors();
        //取出异常对象e中的所有Errors,并将这些Errors的defaultMessage默认错误信息提取出来放入一个新集合中
        List<String> errorsMessages = allErrors.stream()
                .map(error -> error.getDefaultMessage())
                .collect(Collectors.toList());


        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("errorMessage", errorsMessages.toString());//对应视图中的值
        modelAndView.setViewName("error"); // 设置对应的错误页面视图名称

        return modelAndView;
    }

}

异常视图页面

配合异常解析器,将错误变成页面返回前端

导入依赖

<!-- Spring Boot Thymeleaf Starter -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf页面

${errorMessage}是异常响应信息

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
<h1>Hello,An Error</h1>
<p th:text="${errorMessage}"></p>
</body>
</html>

验证

controller

要使用@Valid注解

@RestController
public class IndexController {

	@RequestMapping("/body")
    public Demo test3(@RequestBody @Valid Demo d) {
        System.out.println(d.toString());
        return d;
    }

}

Demo类

import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;

public class Demo {

    private String one;
    private String two;


    private String three;
    private String four;

    public Date getDate() {
        return date;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "one='" + one + '\'' +
                ", two='" + two + '\'' +
                ", three='" + three + '\'' +
                ", four='" + four + '\'' +
                ", date=" + date +
                '}';
    }

    public void setDate(Date date) {
        this.date = date;
    }

    //指定解析日期的格式,请求传入的日期必须是这个格式否则无法解析报400,返回给前端的格式也会是这个
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date date;

    public String getOne() {
        return one;
    }

    public void setOne(String one) {
        this.one = one;
    }

    public String getTwo() {
        return two;
    }

    public void setTwo(String two) {
        this.two = two;
    }

    public String getThree() {
        return three;
    }

    public void setThree(String three) {
        this.three = three;
    }

    public String getFour() {
        return four;
    }

    public void setFour(String four) {
        this.four = four;
    }
}

访问结果

http://localhost:8081/body

{
	"one": "w",
    "two": "2",
    "three": "3",
    "four": "4",
    "date": "2023-10-25"
}

image-20231027100953780

17.注解方式使用验证器

内置注解

image-20231027105650535

扩展注解

image-20231027104903895

说明

@Validator和@Valid的区别

在检验 Controller 的入参是否符合规范时,使用 @Validated 或者 @Valid 在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

分组:

  • @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制

注解使用地方:

  • @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

  • @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。

示例

注解使用

直接加在类属性上,这里规定one属性不可为空

import com.fasterxml.jackson.annotation.JsonFormat;
import javax.validation.constraints.NotNull;
import java.util.Date;

public class Demo {

    @NotNull
    private String one;
    private String two;


    private String three;
    private String four;

    public Date getDate() {
        return date;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "one='" + one + '\'' +
                ", two='" + two + '\'' +
                ", three='" + three + '\'' +
                ", four='" + four + '\'' +
                ", date=" + date +
                '}';
    }

    public void setDate(Date date) {
        this.date = date;
    }

    //指定解析日期的格式,请求传入的日期必须是这个格式否则无法解析报400,返回给前端的格式也会是这个
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date date;


    public String getOne() {
        return one;
    }

    public void setOne(String one) {
        this.one = one;
    }

    public String getTwo() {
        return two;
    }

    public void setTwo(String two) {
        this.two = two;
    }

    public String getThree() {
        return three;
    }

    public void setThree(String three) {
        this.three = three;
    }

    public String getFour() {
        return four;
    }

    public void setFour(String four) {
        this.four = four;
    }
}

异常处理类

用于处理验证错误

import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 处理整个web controller的异常,会把返回值添加到response进行返回
 * */
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ModelAndView methodArgumentNotValidException(MethodArgumentNotValidException e){
        System.out.println("异常是:"+e.getMessage());

        List<ObjectError> allErrors = e.getAllErrors();
        //取出异常对象e中的所有Errors,并将这些Errors的defaultMessage默认错误信息提取出来放入一个新集合中
        List<String> errorsMessages = allErrors.stream()
                .map(error -> error.getDefaultMessage())
                .collect(Collectors.toList());


        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("errorMessage", errorsMessages.toString());
        modelAndView.setViewName("error"); // 设置对应的错误页面视图名称

        return modelAndView;
    }
    
}

配合异常处理的视图

配合异常解析器,将错误变成页面返回前端

导入依赖

<!-- Spring Boot Thymeleaf Starter -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf页面

${errorMessage}是异常响应信息

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
<h1>Hello,An Error</h1>
<p th:text="${errorMessage}"></p>
</body>
</html>

Controller

import javax.validation.Valid;

@RestController
public class IndexController {
    
    @RequestMapping("/body") //
    public Demo test3(@RequestBody @Valid Demo d) {
        System.out.println(d.toString());
        return d;
    }
}

结果

访问

http://localhost:8081/body

故意将数据的one去除

{
	
    "two": "2",
    "three": "3",
    "four": "4",
    "date": "2023-10-25"
}

结果为:

image-20231027105542182

自定义注解

实现一个请求参数中的手机号格式验证

前置准备

用到了hutool工具包

<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.7.13</version>
</dependency>

这是一个手机号格式枚举类,用于结合hutool的手机号工具类PhoneUtil做格式验证

/**
 *指定校验逻辑使用
 */
public enum PhoneModeEnum {
    /**
     * 香港手机号
     */
    IS_MOBILE_HK,
    /**
     * 台湾手机号
     */
    IS_MOBILE_TW,
    /**
     * 澳门手机号
     */
    IS_MOBILE_MO,
    /**
     * 大陆手机号
     */
    IS_MOBILE,
    /**
     * 中国手机号
     */
    IS_PHONE;
}

注解类

自定义的注解类,与@NotNull类似

其中@Constraint指定了此注解要使用的校验逻辑类,PhoneModeEnum value()指定手机号类型

import java.lang.annotation.*;
import javax.validation.Constraint;
import javax.validation.Payload;

//指定本注解生效在成员变量与方法参数上
@Target({ElementType.FIELD, ElementType.PARAMETER})
//运行时生效
@Retention(RetentionPolicy.RUNTIME)
//指定校验器,即用哪个校验器做业务验证
@Constraint(validatedBy = CheckPhoneValidator.class)
public @interface CheckPhone {

    /**
     * 默认提示信息
     * @return
     */
    String message() default "默认的提示!!";

    /**
     *分组使用
     * @return
     */
    Class<?>[] groups() default { };

    /**
     * 在ValidatorFactory初始化期间定义约束验证器有效负载
     * @return
     */
    Class<? extends Payload>[] payload() default { };

    /**
     * 指定使用什么逻辑校验手机号
     * @return
     */
    PhoneModeEnum value();
    
}

实体类

包含手机号地区枚举与实体User类,注意将@CheckPhone加到手机号成员变量上

import lombok.Data;

@Data //隐式添加所有属性的get、set方法
public class User {
    
    private String name;
    private Integer age;

    //指定用哪个地区的手机号格式做验证,并指定了格式不对的提示信息
    @CheckPhone(value = PhoneModeEnum.IS_PHONE,message = "${validatedValue}"+"手机号码格式异常!")
    private String phoneNumber;
}

校验器

initialize方法用于获取@CheckPhone中的PhoneModeEnum value(),以便在isValid方法上做校验

isValid方法的参数mobile,就是被 @CheckPhone 注解标记的实体类属性值,这里是实体类 UserphoneNumber 属性值

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.PhoneUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 自定义校验逻辑
 */
public class CheckPhoneValidator implements ConstraintValidator<CheckPhone, String> {

    private PhoneModeEnum phoneMode;

    /**
     * initialize()方法可以访问已验证约束的属性值,并允许您将它们存储在验证器的字段中
	 *
	 * 这里的 constraintAnnotation.value() 取得就是 @CheckPhone 的 value 值,
	 * 即:PhoneModeEnum.IS_PHONE
	 *
     */
    @Override
    public void initialize(CheckPhone constraintAnnotation) {

        this.phoneMode = constraintAnnotation.value();
    }

    /**
     * isValid()方法包含实际的验证逻辑
	 *
     * @param mobile 被 @CheckPhone 注解标记的实体类属性值,这里是 User 的 phoneNumber
	 *
     * @param constraintContext
	 *
     * @return 返回false校验失败
     */
    @Override
    public boolean isValid(String mobile, ConstraintValidatorContext constraintContext) {
       if (ObjectUtil.isNull(mobile)) {
            return true;
        }
       	//PhoneUtil 是 hutool 的手机号验证工具类
        if (ObjectUtil.equal(phoneMode ,PhoneModeEnum.IS_MOBILE_HK)) {
            return PhoneUtil.isMobileHk(mobile);
        } else if ((ObjectUtil.equal(phoneMode ,PhoneModeEnum.IS_MOBILE_TW))){
            return PhoneUtil.isMobileTw(mobile);
        } else if ((ObjectUtil.equal(phoneMode ,PhoneModeEnum.IS_MOBILE_MO))){
            return PhoneUtil.isMobileMo(mobile);
        } else if ((ObjectUtil.equal(phoneMode ,PhoneModeEnum.IS_MOBILE))){
            return PhoneUtil.isMobile(mobile);
        } else {
            return PhoneUtil.isPhone(mobile);
        }

    }
}

异常处理器

异常处理器

import cn.hutool.core.collection.CollUtil;
import com.pig4cloud.pigx.common.core.util.R;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 处理整个web controller的参数校验
 *
* */
@ControllerAdvice
public class ValidExceptionHandler {

	//处理方法参数加 @Valid 或 @Validated 的
    @ExceptionHandler(MethodArgumentNotValidException.class)
	@ResponseBody
    public R handlerMethodArgumentNotValidException(MethodArgumentNotValidException e){
		BindingResult bindingResult = e.getBindingResult();
		if (bindingResult.hasErrors()){
			return R.failed(
					bindingResult.getAllErrors().stream()
							.map(error ->error.getDefaultMessage()).collect(Collectors.joining(",")));
		}
		return null;
    }

}

Controller

import com.pig4cloud.pigx.common.core.util.R;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("mytest")
public class MyController {

	@PostMapping("myPhone")
	public R myPhone(@RequestBody @Valid User user){

		return R.ok(user.getPhoneNumber());
	}

}

效果验证

访问

http://ip:port/mytest/myPhone

数据

{
    "name": "wzy",
    "age": 28,
    "phoneNumber": "15230113576"
}

效果:

返回成功

在这里插入图片描述

然后输入一个错误格式的手机号,111开头的,则提示手机号码格式异常,对应上了@CheckPhone注解的message自定义错误i提示:
在这里插入图片描述

18.配置消息代码解析器

使用消息代码解析器可以实现根据验证错误的消息代码,生成对应的错误消息。需要配合验证器Validator或对应的验证注解使用。

下面看示例:实现非空检验@NotNull(message = "Demo.one.notNull")message转换,将错误码转为具体错误消息

自定义消息代码解析器

import org.springframework.validation.MessageCodesResolver;

public class CustomMessageCodesResolver implements MessageCodesResolver {

    @Override
    public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class<?> fieldType) {
        String[] codes = new String[2];
        codes[0] = errorCode;
        codes[1] = objectName + "的属性" + field + "必须" + errorCode;
        return codes;
    }

    @Override
    public String[] resolveMessageCodes(String errorCode, String objectName) {
        String[] codes = new String[2];
        codes[0] = errorCode;
        codes[1] = objectName + "的属性" + errorCode;
        return codes;
    }
}

配置类

import com.example.code.CustomMessageCodesResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class webConfig implements WebMvcConfigurer {
    
    @Override
    public MessageCodesResolver getMessageCodesResolver() {
        return new CustomMessageCodesResolver();
    }

}

业务类

//省略get、set
public class Demo {

    @NotNull(message = "Demo.one.notNull")
    private String one;
    private String two;
    private String three;
    private String four;
}

异常处理器及视图

异常处理器

import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 处理整个web controller的异常,会把返回值添加到response进行返回
 * */
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ModelAndView methodArgumentNotValidException(MethodArgumentNotValidException e){
        System.out.println("异常是:"+e.getMessage());

        List<ObjectError> allErrors = e.getAllErrors();
        //取出异常对象e中的所有Errors,并将这些Errors的defaultMessage默认错误信息提取出来放入一个新集合中
        List<String> errorsMessages = allErrors.stream()
                .map(error -> error.getCodes()[1])
                .collect(Collectors.toList());


        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("errorMessage", errorsMessages.toString());
        modelAndView.setViewName("error"); // 设置对应的错误页面视图名称

        return modelAndView;
    }
    
}

视图

配合异常解析器,将错误变成页面返回前端,记得导入依赖

<!-- Spring Boot Thymeleaf Starter -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf页面,${errorMessage}是异常响应信息

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
<h1>Hello,An Error</h1>
<p th:text="${errorMessage}"></p>
</body>
</html>

Controller

import javax.validation.Valid;

@RestController
public class IndexController {
    
    @RequestMapping("/body")
    public Demo test3(@RequestBody @Valid Demo d) {
        System.out.println(d.toString());
        return d;
    }
}

效果验证

访问

http://localhost:8081/body

数据,少写一个one属性

{
    "two": "2",
    "three": "3",
    "four": "4",
    "date": "2023-10-25"
}

效果

image-20231027150252970

19.其他yaml配置

spring:
  mvc:
    #配置静态资源访问前缀, /**表示所有,即如果没有对应的Controller,所有请求都会尝试去访问静态资源。
    #这个配置影响静态资源访问路径,比如配置了 /resource/** 那么访问的时候,路径要加上resource,才能访问静态资源
    static-path-pattern: /**
    #enabled: true 表示启用了隐藏 HTTP 方法过滤器。
    #允许客户端发送 POST 请求,并在请求中添加一个隐藏字段 _method,用于指定实际的 HTTP 方法。
    #例如,可以通过将 _method 字段的值设置为 "PUT",从而将 POST 请求转换成 PUT 请求。
    hiddenmethod:
      filter:
        enabled: true
   
   
  #配置静态资源访问路径,即访问静态资源时,去下面的项目路径中去找静态资源,按照先后顺序
  #注意,此项配置不能和静态资源访问前缀同时配置,除非静态资源访问前缀使用的是默认的/**
  resources:
    static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/templates/   
   
   
  #指定 Spring Boot 应用程序的 HTTP 编码配置
  http:
    encoding:
      #强制设置字符编码。如果设置为 true,则会在响应头中强制设置字符编码为指定的值。
      force: true
      #指定了字符编码为 UTF-8。当 force 属性为 true 时,这个字符编码将会被强制设置到响应头中。
      charset: UTF-8
      #启用 HTTP 编码配置
      enabled: true    

hiddenmethod隐藏表单提交

在 HTML 表单中,由于浏览器仅支持 GET 和 POST 方法,如果需要使用其他方法(如 PUT、DELETE),可以通过添加一个隐藏字段 _method 来模拟发送对应的请求方法。以下是一个示例代码:

<form action="/example" method="POST">
    <!-- 添加隐藏字段 _method,并设置其值为 PUT -->
    <input type="hidden" name="_method" value="PUT">
    <!-- 其他表单字段 -->
    <input type="text" name="name">
    <input type="submit" value="Submit">
</form>

在这个示例中,当用户提交表单时,实际上是通过 POST 方法提交的。但由于添加了隐藏字段 _method,并将其值设置为 “PUT”,服务器会根据这个值来识别实际的请求方法为 PUT。这样就可以在不支持 PUT 方法的环境中模拟发送 PUT 请求。

类似地,也可以将 _method 的值设置为 “DELETE” 或其他 HTTP 方法,以模拟发送对应的请求。在服务器端接收到请求时,需要对这个隐藏字段进行解析,并根据其值来处理相应的业务逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值