spring mvc

Spring MVC

1. 初识Spring MVC

1.1. spring mvc 的流程

在springboot中,我们可以很轻易地完成进行web开发,只需要添加@Controller等注解即可,但是springboot底层是使用spring mvc完成的我们的知道他的运行流程,初始化了什么组件,组件有什么功能,各组件之间的练习。spring mvc是以DispatcherServlet为核心的配合其他组件进行工作。spring mvc的工作流程如下图所示:

image-20201106150249276

在web容器初始化的时候,@Controller和@RequstMapping等注解内容会被扫描到HandlerMapping机制中存储起来。用户在发起请求之后,被DispatcherServlet拦截下来,得到URL和其他条件,与HandlerMapping进行匹配,找到对应的处理器,并将处理器和拦截器保存到HandlerExecutionChain中,把他返回给DispatcherServlet。

1.2 定制MVC的初始化

想要自定义mvc 的初始化配置可以在配置文件中设置或者自定义一个配置类实现WebMvcConfigurer接口,那在配置文件中和配置类中的内容是由配置类WebMvcAutoConfiguration来定义的,它里面有一个内部类WebMvcAutoConfigurationAdapter实现了WebMvcConfigurer,通过它,springboot就能自动的配置spring mvc的初始化。

1.3 获取前端传递的参数

书中使用jsp文件做前端展示与后端交互的,所以看这篇博客在springboot中使用jsp

  1. 通过URL传递参数

    @RequestParam它可以使前端URL传过来的参数和后端方法里的参数相对应。

    public Map<String, Object> insertUser(@RequestParam("user_name") String userName, String note) 
    
  2. 传递数组,前端传过来的数组元素之间必须以,隔开才能被正确接收

    如http://localhost:8080?init_val=1,2,3

    public int test(@RequestParam("init_val") int[] initVal)
    
  3. 传递json

    前端传过来的数据如果是Json格式的,要使用@ResponseBody来接收,spring mvc会自动将json里面的数据映射为@ResponseBody修饰的参数,要求Json里面的属性名称和User对象的属性名称是一致的

    public int test(@RequestBody User user)
    
  4. 通过URL路径传递参数

    使用@PathVariable配合@RequestMapping可以获取URL路径中的值

    @RequestMapping("/insertUsers/{id}")
    @ResponseBody
    public int test(@PathVariable int id) {
    
  5. 获取格式化参数

    在系统开发中,需要格式化数据,日期和金钱最为常见,如:系统中日期格式的约定是yyyy-MM-dd,金额约定以$和,分隔,日期约定可以使用@DataTimeFormat,金钱约定可以使用@NumberFormat

    public Map<String,Object> commit(
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date,
            @NumberFormat(pattern = "#,###.##") Double money
    
    ){
    

2.深入学习SpirngMVC

2.1 参数转换流程

除了springMVC的参数转换规则外,我们还可以自定义转换规则

如果是简单的参数,springMVC可以通过已经定义好的简单转换器进行转换,如果是http请求体那么会通过HttpMessageConverter来进行转换

public interface HttpMessageConverter<T> {
    //是否可读,Class<?> var1:java类型, MediaType var2 :http请求类型
    boolean canRead(Class<?> var1, @Nullable MediaType var2);
	//是否可写,判断是否能将java类型转换为http响应类型,Class<?> var1:java类型, MediaType var2 :http响应类型
    boolean canWrite(Class<?> var1, @Nullable MediaType var2);
	//获取支持的媒体类型列表
    List<MediaType> getSupportedMediaTypes();
	//canRead为true后执行,将java类型读入HttpInputMessage
    T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
	//canWrite通过以后,将java类型写入响应
    void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}

例如上面的获取JSON类型,因为控制器方法的参数使用了@RequestBody,所以处理器会采用请求体的内容进行参数转换,先调用canRead判断请求体是否可读,然后调用read方法将请求体提交的JSON类型转换为控制器的参数User类型。

HttpMessageConverter接口中的canRead和read方法就是把http的请求体转换为java类型的。但是还有其他的工作比如前端转换过来的是一个整数,但是想要将它转换成一个枚举类型,当然也可以使用VO对象,在处理器中处理,但是spring可以自定义参数转换规则,当然不用这么麻烦。

获取参数,转换参数和数据的验证的过程是由WebDataBinder来完成的。先获取参数,然后通过三种接口GenericConverter,Formatter,Converter来实现参数的转换,springMVC中,有这三个接口的实现类,都采用了 注册机机制。可以完成很多参数的转换(如 integer string。。。)。那么要自定义参数转换规则,只需要在注册机中注册自己的转换器即可。

springMVC处理HTTP请求体的流程图如下图所示

image-2020110911032926

GenericConverter,Formatter,Converter

Converter是一个简单的转换器,例如参数是Integer类型,参数是字符串,那么Converter就会把字符串转成Integer

Formatter:是一个格式转换器,例如类似日期字符串就是通过Formatter按照约定的格式转成Date类型

GenericConverter将http参数转换为数组???

数据类型转换,springMVC提供了服务机制ConversionService去管理这些转换规则,默认通过DefaultFormattingConversionService来管理这些转换类。

image-20201109111259113

GenericConverter,Formatter,Converter可以通过他们自己的注册机接口进行注册,这样处理器就能获取对应的转换类来进行参数转换。

SpringBoot中,通过WebMvcAutoConfigurationAdapter类中的addFormatters方法来进行注册,源码如下

public void addFormatters(FormatterRegistry registry) {
    ApplicationConversionService.addBeans(registry, this.beanFactory);
}
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
    Set<Object> beans = new LinkedHashSet();
    beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
    beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
    beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
    beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
    Iterator var3 = beans.iterator();

    while(var3.hasNext()) {
        Object bean = var3.next();
        if (bean instanceof GenericConverter) {
            registry.addConverter((GenericConverter)bean);
        } else if (bean instanceof Converter) {
            registry.addConverter((Converter)bean);
        } else if (bean instanceof Formatter) {
            registry.addFormatter((Formatter)bean);
        } else if (bean instanceof Printer) {
            registry.addPrinter((Printer)bean);
        } else if (bean instanceof Parser) {
            registry.addParser((Parser)bean);
        }
    }
}

2.2 Converter接口,一对一转换器

public interface Converter<S, T> {
	//S:源,T:目标类型	
   T convert(S source);
}

需求: 将格式为id-userName-note的字符串转换成user对象

@Component//boot会自动扫描,并注册到转换机制中
public class StringToUserConverter implements Converter<String, TUser> {
    @Override
    public TUser convert(String source) {
        TUser tuser = new TUser();
        String[] split = source.split("-");
        tuser.setId(Long.parseLong(split[0]));
        tuser.setUserName(split[1]);
        tuser.setNote(split[2]);
        return tuser;
    }
}

前面讲过只要加上Component继承了Converter,springboot就能自动识别并注册该转换器到参数转换机制中。

但很遗憾虽然已经注册进去了,但不知道什么原因不起作用???测试链接为:http://localhost:8080/user/convert?user=11-iandf-student

是因为参数上没有加@RequestParam("user")注解,加上就成功了

2.3 验证机制

spring可以引入依赖,使用JSR-303的注解进行验证

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
public class ValidatorPojo {
   // 非空判断
   @NotNull(message = "id不能为空")
   private Long id;

   @Future(message = "需要一个将来日期") // 只能是将来的日期
   // @Past //只能去过去的日期
   @DateTimeFormat(pattern = "yyyy-MM-dd") // 日期格式化转换
   @NotNull // 不能为空
   private Date date;

   @NotNull // 不能为空
   @DecimalMin(value = "0.1") // 最小值0.1元
   @DecimalMax(value = "10000.00") // 最大值10000元
   private Double doubleValue = null;

   @Min(value = 1, message = "最小值为1") // 最小值为1
   @Max(value = 88, message = "最大值为88") // 最大值88
   @NotNull // 不能为空
   private Integer integer;

   @Range(min = 1, max = 888, message = "范围为1至888") // 限定范围
   private Long range;

   // 邮箱验证
   @Email(message = "邮箱格式错误")
   private String email;

   @Size(min = 20, max = 30, message = "字符串长度要求20到30之间。")
   private String size;
   /**** setter and getter ****/
}

当然有时候这些注解满足不了验证的规则,那么可以使用自定义验证规则,这需要使用到Validator接口,然后将它注入到WebDateBinder中,让它工作。

需求:让用户名字不能为空

public class UserValidator implements Validator {
    //如果是TUser.class则执行当前验证方法
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(TUser.class);
    }
    //当supports为true时执行
    @Override
    public void validate(Object target, Errors errors) {
        // 强制转换
        TUser user = (TUser) target;
        // 用户名非空串
        if (StringUtils.isEmpty(user.getUserName())) {
            // 增加错误,可以进入控制器方法
            errors.rejectValue("userName", "111", "用户名不能为空");
        }
    }
}

需要使用@InitBinder注解的作用是在控制器方法执行前,先执行带有@InitBinder注解的方法,这时可以将WebDateBinder作为参数传递进去,不仅仅可以注册自定义的Validator还可以自定义Date等类型的格式,这样就可以替代@DateTimeFormat

@InitBinder//
public void initBinder(WebDataBinder binder){
    binder.addValidators(new UserValidator());
    //false表示不允许为空
    binder.registerCustomEditor(Date.class,new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),false));
}

测试

@GetMapping("/validator")
@ResponseBody
public Map<String, Object> validator(@Valid TUser user, Errors Errors) {
    Map<String, Object> map = new HashMap<>();
    map.put("user", user);
    // 判断是否存在错误
    if (Errors.hasErrors()) {
        // 获取全部错误
        List<ObjectError> oes = Errors.getAllErrors();
        for (ObjectError oe : oes) {
            // 判定是否字段错误
            if (oe instanceof FieldError) {
                // 字段错误
                FieldError fe = (FieldError) oe;
                map.put(fe.getField(), fe.getDefaultMessage());
            } else {
                // 对象错误
                map.put(oe.getObjectName(), oe.getDefaultMessage());
            }
        }
    }
    return map;
}

@Valid注解,当spring mvc遇到带有@Valid注解的参数时,回去便利验证器,对User对象进行验证。

测试成功

2.4 数据模型

数据模型的作用是绑定数据,为后面的视图渲染做准备的。Spring使用的数据模型接口和类的继承关系如下

image-20201110090538811

只要在控制器的方法参数中使用Model ModelMap ModelAndView spring mvc都会为其创建数据模型。

测试

@RequestMapping("/model")
public String model(Long id, Model model){
    TUser tUser = userMapper.selectByPrimaryKey(id);
    model.addAttribute("user",tUser);
    return "/data/user";
}

@RequestMapping("/modelMap")
public ModelAndView modelMap(Long id, ModelMap modelMap){
    TUser tUser = userMapper.selectByPrimaryKey(id);
    ModelAndView mv = new ModelAndView();
    mv.setViewName("/data/user");
    modelMap.addAttribute("user",tUser);
    return mv;
}

@RequestMapping("/modelAndView")
public ModelAndView modelMap(Long id, ModelAndView mv){
    TUser tUser = userMapper.selectByPrimaryKey(id);
    mv.setViewName("/data/user");
    mv.addObject("user",tUser);
    return mv;
}

这三种都能成功完成试图渲染,尽管第二种的modelMap没有和ModelAndView绑定。spring mvc会自动的绑定它。

2.5 视图设计

spring mvc提供了很多视图,但这些视图都继承了View接口

public interface View {
    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
    String PATH_VARIABLES = View.class.getName() + ".pathVariables";
    String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";

    @Nullable
    default String getContentType() {
        return null;
    }

    void render(@Nullable Map<String, ?> var1, HttpServletRequest var2, HttpServletResponse var3) throws Exception;
}

getContentType得到Http的响应类型,类型可以使Json,文件等。render方法则是将数据模型渲染到视图的。

spring mvc提供的视图接口和类如下:

image-20201110092624752

只有逻辑视图才用到视图解析器。

需求:将所有用户使用pdf文档导出并展示。

PDF视图不是逻辑视图,有上表所知,需要实现AbstratctPdfView。需要实现一个方法

protected abstract void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
                                         HttpServletRequest request, HttpServletResponse response)

所以自定义PdfView实现这个类,由于每个控制器想要导出的Pdf格式不一致,所以还有创建一个导出服务接口,让各个控制器自己写导出的Pdf的逻辑(标题,字体颜色等等)

//这个接口由控制器实现,然后把它注入到PdfView中。
public interface PdfExportService {
    void buildPdfDocument(Map<String, Object> map, Document document, PdfWriter pdfWriter, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse);
}
public class PdfView extends AbstractPdfView {
    PdfExportService exportService;

    public PdfView(PdfExportService exportService) {
        this.exportService = exportService;
    }

    @Override
    protected void buildPdfDocument(Map<String, Object> map, Document document, PdfWriter pdfWriter, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        //让各个控制器自定义导出Pdf的格式等等
        exportService.buildPdfDocument(map,document,pdfWriter,httpServletRequest,httpServletResponse);
    }
}

测试

@RequestMapping("/export/pdf")
public ModelAndView exportPdf(ModelAndView mv){
    List<TUser> userList = userMapper.selectAll();
    //准备视图
    PdfView view = new PdfView(exportService());
    mv.addObject("userList",userList);
    mv.setView(view);
    return mv;
}

private PdfExportService exportService() {
    // 使用Lambda表达式定义自定义导出
    return (model, document, writer, request, response) -> {
        try {
            // A4纸张
            document.setPageSize(PageSize.A4);
            // 标题
            document.addTitle("用户信息");
            // 换行
            document.add(new Chunk("
"));
            // 表格,3列
            PdfPTable table = new PdfPTable(3);
            // 单元格
            PdfPCell cell = null;
            // 字体,定义为蓝色加粗
            Font f8 = new Font();
            f8.setColor(Color.BLUE);
            f8.setStyle(Font.BOLD);
            // 标题
            cell = new PdfPCell(new Paragraph("id", f8));
            // 居中对齐
            cell.setHorizontalAlignment(1);
            // 将单元格加入表格
            table.addCell(cell);
            cell = new PdfPCell(new Paragraph("user_name", f8));
            // 居中对齐
            cell.setHorizontalAlignment(1);
            table.addCell(cell);
            cell = new PdfPCell(new Paragraph("note", f8));
            cell.setHorizontalAlignment(1);
            table.addCell(cell);
            // 获取数据模型中的用户列表
            List<TUser> userList = (List<TUser>) model.get("userList");
            for (TUser user : userList) {
                document.add(new Chunk("
"));
                cell = new PdfPCell(new Paragraph(user.getId() + ""));
                table.addCell(cell);
                cell = new PdfPCell(new Paragraph(user.getUserName()));
                table.addCell(cell);
                String note = user.getNote() == null ? "" : user.getNote();
                cell = new PdfPCell(new Paragraph(note));
                table.addCell(cell);
            }
            // 在文档中加入表格
            document.add(table);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    };
}
image-2020111009473404

pdf视图就被成功渲染出来了

2.6 文件上传

DispartcherServlet会使用适配器模式将HttpServeletRequest转换为MultipartHttpServletRequest。

HttpServeletRequest与MultipartHttpServletRequest关系图如下:

image-20201110103216891

MultipartHttpServletRequest同时继承了HttpServeletRequest和MultipartRequest(里面包含了操作文件的方法)。

如何判断是否将HttpServeletRequest转换为MultipartHttpServletRequest还需要通过MultipartResolver,推荐使用StandardServletMultipartResolver

MultipartResolver接口和类图如下:

image-20201110103646423

StandardServletMultipartResolver部分源码如下:

public boolean isMultipart(HttpServletRequest request) {
    return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}

public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
    return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}

StandardServletMultipartResolver会被默认创建,为了更加灵活还可以在配置文件中自定义配置,具体配置看MultipartProperties类。

  servlet:
    multipart:
      enabled: true #Whether to enable support of multipart uploads.
      location: e:/springboot/upload #Intermediate location of uploaded files.
      max-file-size: 5MB #maxFileSize
      max-request-size: 20MB #上传所有文件最大的容量

需求,上传一个word文档到e:/springboot/upload目录下

<%--这边的enctype必须指定为multipart/,否则MultipartResolver不会转换HttpServletRequest--%>
<form method="post" 
        action="./request" enctype="multipart/form-data">
    <input type="file" name="file" value="请选择上传的文件" /> 
    <input type="submit" value="提交" />
</form>
@PostMapping("/upload/request")
@ResponseBody
public Map<String,Object> uploadRequest(HttpServletRequest request){
    MultipartHttpServletRequest multipartHttpServletRequest = null;
    HashMap<String, Object> resultMap = new HashMap<>();
    if (request instanceof MultipartHttpServletRequest) {
        multipartHttpServletRequest = (MultipartHttpServletRequest) request;
    }else {
        return dealResultMap(false,"上传失败");
    }
    //获取MultipartFile对象
    MultipartFile multipartFile = multipartHttpServletRequest.getFile("file");
    String filename = multipartFile.getOriginalFilename();
    //创建新文件,指定文件名
    File file = new File(filename);
    try {
        //将文件保存至配置的地点,e:/springboot/upload
        multipartFile.transferTo(file);
    } catch (IOException e) {
        e.printStackTrace();
        return dealResultMap(false,"上传失败");
    }
    return dealResultMap(true,"上传成功");
}

结果

image-20201110110107922

对于文件的上传可以使用 Servlet API提供的Part接口或者 Spring MVC提供的Multipartfile接口作为参数。其实无论使用哪个类都是允许的,只是我更加推荐使用的是Part,因为毕竟 Multipartfile是 Spring MVC提供的第三方包才能进行支持的,后续版本变化的概率略大一些。

@PostMapping("/upload/multi")
@ResponseBody
public Map<String,Object> uploadMulti(@RequestParam("file") MultipartFile multipartFile){
    String filename = multipartFile.getOriginalFilename();
    //创建新文件,指定文件名
    File file = new File(filename);
    try {
        //将文件保存至配置的地点,e:/springboot/upload
        multipartFile.transferTo(file);
    } catch (IOException e) {
        e.printStackTrace();
        return dealResultMap(false,"上传失败");
    }
    return dealResultMap(true,"上传成功");
}

@PostMapping("/upload/part")
@ResponseBody
public Map<String,Object> uploadPart(@RequestParam("file") Part sourceFile){
    String filename = sourceFile.getSubmittedFileName();
    try {
        //将文件保存至配置的地点,e:/springboot/upload
        sourceFile.write(filename);
    } catch (IOException e) {
        e.printStackTrace();
        return dealResultMap(false,"上传失败");
    }
    return dealResultMap(true,"上传成功");
}

2.7 拦截器

2.7.1 拦截器简介

所有的拦截器都要实现HandlerInterceptor接口

public interface HandlerInterceptor {
	//preHandle方法在处理器执行前调用
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}
	//postHandle在处理器处理后,视图渲染前调用
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}
	//afterCompletion在视图渲染后调用
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

拦截器的执行流程如下:

image-20201110161211225

处理器的逻辑包含了控制器功能。

2.7.2 单个拦截器

验证处理器执行流程

@Component
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle...处理器处理前");
        //返回true不会拦截后续的处理
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...处理器处理后,视图渲染前");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("postHandle...视图渲染后");
    }
}

拦截器不会自动的加载到spring mvc的机制中,需要在配置类中指定

@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    private MyInterceptor myInterceptor;

    public MyWebConfig(MyInterceptor myInterceptor) {
        this.myInterceptor = myInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //将拦截器添加到spring mvc机制,返回一个拦截器注册
        InterceptorRegistration interceptorRegistration = registry.addInterceptor(myInterceptor);
        //配置拦截匹配模式,拦截指定的URL
        interceptorRegistration.addPathPatterns("/interceptor/*");
    }
}

测试

@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
    @GetMapping("/start")
    public String start(){
        System.out.println("执行处理器逻辑");
        return "/welcome";
    }
}
<body>
    <h1><%
    System.out.println("视图渲染");
    out.print("欢迎学习Spring Boot MVC章节
");
    %></h1>
</body>

image-20201110164107109

preHandle返回值改为false之后,执行完preHandle就直接是完成请求的状态,状态码是200

2.7.3 多个拦截器

写拦截器MultiInterceptor1,MultiInterceptor2,MultiInterceptor3

@Component
public class MultiInterceptor1 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("["+this.getClass().getSimpleName()+"]"+"preHandle...处理器处理前");
        //返回true不会拦截后续的处理
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("["+this.getClass().getSimpleName()+"]"+"postHandle...处理器处理后,视图渲染前");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("["+this.getClass().getSimpleName()+"]"+"postHandle...视图渲染后");
    }
}

注册拦截器

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MultiInterceptor1()).addPathPatterns("/interceptor/*");
    registry.addInterceptor(new MultiInterceptor2()).addPathPatterns("/interceptor/*");
    registry.addInterceptor(new MultiInterceptor3()).addPathPatterns("/interceptor/*");
}

测试

image-2020111016400901

preHandle方法时先注册限制行,postHandle和afterCompletion是先注册后执行,遵循责任链模式。

那么多个拦截器,把MultiInterceptor2的preHandle方法改为false之后执行流程是这样的

image-20201110163822017

从上面的日志可以看出,处理器前( preHandle)方法会执行,但是一旦返回 false,则后续的拦截器、处理器和所有拦截器的处理器后( postHandle)方法都不会被执行。完成方法 afterCompletion则不一样,它只会执行返回true的拦截器的完成方法,而且顺序是先注册后执行

3. spring mvc的其他知识

3.1 @ReponseBody转换Json的秘密

在进入控制器方法前,当遇到标注的@Responsebody后,处理器就会记录这个方法的响应类型为JSON数据集。当执行完控制器返回后,处理器会启用结果解析器( Resultresolver)去解析这个结果,它会去轮询注册给 Spring MVC的 Httpmessageconverter接口的实现类。因为 Mapping Jackson2HttpmessageConverter这个实现类已经被 Spring MVC所注册,加上 Spring MVC将控制器的结果类型标明为JSON,所以就匹配上了,于是通过它就在处理器内部把结果转换为了JSON。当然有时候会轮询不到匹配的HttpmessageConverter,那么它就会交由 Spring MVC后续流程去处理。如果控制器返回结果MappingJackson2HttpmessageConverter进行了转换,那么后续的模型和视图( ModelAndview)就返null,这样视图解析器和视图渲染就不会工作了

@ReponseBody准换Json的流程图

image-20201110165339531

3.2 重定向

重定向使用“redirect”开头的字符串,后面跟URL。

在重定向中如果想要保存前一条请求带来的数据要使用RedirectAttributes

redirectAttributes.addFlashAttribute("name","value");

Model是不能携带数据到下一个请求的。RedirectAttributes是先把数据放在Session中,在重定向请求之后,从Session中取出数据,填充到重定向的参数和数据模型中,再删除session中的相关数据,流程图如下:

image-20201110170114658

3.3 给控制器添加通知

之前可以通过AOP给bean类添加通知,spring mvc也提供了注解给控制器添加通知,主要是通过以下四个注解完成

@ControllerAdvice//定义一个控制器通知类,用来增强控制器的功能
@InitBinder//用来给WebDataBinder绑定参数准换规则,定义参数的格式和数据验证等。
@ModelAttribute//控制器方法执行前,往数据模型中添加信息
@ExceptionHandler(value = Exception.class)//控制器执行过程中出了异常就由这个方法处理
@ControllerAdvice(
        basePackages = "com.yogurt.chapter9.controller.advice",
        annotations = Controller.class
)//定义一个控制器通知类,用来增强控制器的功能
public class MyControllerAdvice {

    @InitBinder//用来给WebDataBinder绑定参数准换规则,定义参数的格式和数据验证等。
    public void initBinder(WebDataBinder dataBinder){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dataBinder.registerCustomEditor(Date.class,new CustomDateEditor(simpleDateFormat,false));
    }

    //控制器方法执行前,往数据模型中添加信息
    @ModelAttribute
    public void modelAttribute(Model model){
        model.addAttribute("project_name","chapter9");
    }

    @ExceptionHandler(value = Exception.class)//控制器执行过程中出了异常就由这个方法处理
    public String exceptionHandler(Model model,Exception exception){
        //给异常模型添加信息
        model.addAttribute("exception_message",exception.getMessage());
        //返回异常视图
        return "/exception";
    }

}

测试

@Controller
@RequestMapping("/advice")
public class AdviceController {
    @GetMapping("/test")
    // 因为日期格式被控制器通知限定,所以无法再给出
    public String test(@RequestParam("date") Date date, ModelMap modelMap) {
        // 从数据模型中获取数据
        System.out.println(modelMap.get("project_name"));
        // 打印日期参数
        System.out.println(date.toString());
        // 抛出异常,这样流转到控制器异常通知
        throw new RuntimeException("异常了,跳转到控制器通知的异常信息里");
    }
}

测试无误

3.4 获取请求头参数

使用@RequestHeader注解获取请求头参数

测试

@GetMapping("/header/page")
public String headerPage() {
    return "header";
}

@PostMapping("/header/user")
@ResponseBody
// 通过@RequestHeader接收请求头参数
public TUser headerUser(@RequestHeader("id") Long id) {
    TUser user = userMapper.selectByPrimaryKey(id);
    return user;
}
$.post({
    url : "./user",
    // 设置请求头参数
    headers : {id : '77'},
    // 成功后的方法
    success : function(user) {
        if (user == null || user.id == null) {
            alert("获取失败");
            return;
        }
        // 弹出请求返回的用户信息
        alert("id=" + user.id +", user_name="
              +user.userName+", note="+ user.note);
    }
});
原文地址:https://www.cnblogs.com/iandf/p/13955634.html