Spring Core 官方文档阅读笔记(十六)

1. FormData

浏览器只支持通过Http GET和Http POST来提交表单数据,但是非浏览器客户端可以使用Http PUT、PATCH或者DELETE请求。ServletAPI要求ServletRequest.getParameter*()方法仅支持HTTP POST的表单字段访问。

2. Forwarded Headers

当请求通过代理时,host、port和schema可能会发生改变,我们需要在客户端创建指向正确路径的链路。RFC 7239定义了转发的HTTP报头,代理可以使用该报头来提供有关原始请求的信息。还有其他非标准报头,包括X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-SSL和X-Forwarded-Prefix。ForwardedHeaderFilter是一个servlet筛选器,它可以从上述报头中获取跳转信息,使请求按照正确的链路发送,并将跳转信息隐藏。但是,由于应用程序无法知道这些报头是否是合法添加的,所以存在安全隐患。因此位于信任边界的代理应该配置为删除来自外部的不受信任的转发报头。还可以使用removeOnly=true配置ForwardedHeaderFilter,在这种情况下,它会删除但不使用报头。

3. Shallow ETag

ShallowEtagHeaderFilter过滤器通过缓存写入响应的内容并从中计算MD5散列来创建“shallow”Etag。下一次客户端发送时,它会执行相同的操作,但它还会将计算出的值与if-none-match请求报头进行比较,如果两者相等,则返回304(NOT_MODIFIED)。这种策略可以节省网络带宽,但不能节省CPU,因为必须为每个请求计算完整的响应。前面描述的控制器级的其他策略可以避免计算。请参见HTTP缓存。此过滤器具有writeWeakETag参数,该参数将过滤器配置为写入弱eTag,如下所示:W/“02a2d595e6ed9a0b24f027f2b63b134d6”(如RFC 7232第2.3节所定义)。

4. CORS

spring MVC通过控制器上的注释提供对CORS配置的细粒度支持。然而,当与Spring Security一起使用时,建议依赖内置的CorsFilter,这些CorsFilter必须在Spring Security的过滤器链之前执行。

5. Annotated Controller

Spring MVC提供了一个基于注解的编程模型,如@Controller、@RestController。先来看个例子:

@Controller
public class MyController {
    
    @GetMapping("/hello")
    public String hello(Model model) {
        model.setAttribute("message", "Hello World!");
        return "index";
    }
}

@Controller允许自动检测,假设Controller位于com.web包内,看代码:

@Configuration
@ComponentScan("com.web")
public class WebConfig {
    ... ...   
}

这样就可以让Controller被Spring自动检测。也可以通过xml配置达到同样的效果:

<context:component-scan base-package="com.web" />

@RestController是一个复合注解,我们看一下源码:

/**
 * A convenience annotation that is itself annotated with
 * {@link Controller @Controller} and {@link ResponseBody @ResponseBody}.
 * <p>
 * Types that carry this annotation are treated as controllers where
 * {@link RequestMapping @RequestMapping} methods assume
 * {@link ResponseBody @ResponseBody} semantics by default.
 *
 * <p><b>NOTE:</b> {@code @RestController} is processed if an appropriate
 * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
 * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter}
 * pair which are the default in the MVC Java config and the MVC namespace.
 *
 * @author Rossen Stoyanchev
 * @author Sam Brannen
 * @since 4.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 * @since 4.0.1
	 */
	@AliasFor(annotation = Controller.class)
	String value() default "";

}

@Target和@Retention是用来定义注解用的,@Documented是用来标识将被javadoc记录。再下面就可以看到有@Controller和@ResponseBody两个注解,也就是说,@RestController包含了@Controller和@ResponseBody,表明了Controller的每一个方法都继承类型级别的@ResponseBody注解,因此,返回值直接写入响应主体,而不是使用HTML模板进行视图解析和呈现。

6. RequestMapping

我们可以通过@RequestMapping建立请求和Controller之间的映射关系。可以通过URL、HttpMethod、请求参数、请求头和媒体类型来进行匹配。@RequestMapping注解可以放在类上或者方法上。

@RequestMapping的衍生注解有:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

看个例子:

@RestController
@RequestMapping("/person")
public class MyController {
    
    @GetMapping("/{id}")
    public Person getPerson(@PathVariable String id) {
        // ...
    }
    
    @PostMapping()
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}

上面代码中出现了一个@PathVariable注解,这个就是用来获取URL上的"{id}"参数。我们也可以通过正则表达式来匹配,如:

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}

如果有多个被匹配上,则会对这些备选项进行比较,取最佳项。一般来说,会选择最精准的匹配项。

在一些情况下,请求路径中可能含有文件的扩展名,出于安全的考虑,在匹配路径时,我们需要将扩展名禁用,要完全禁用文件扩展名的使用,必须同时设置以下两项:

PathMatchConfigurer.useSuffixPatternMatching(false);
ContentNegotiationConfigurer.favoritePathExtension(false);

如果必须使用文件扩展名,推荐使用ContentNegotiationConfigurer的mediaTypes属性将其限制为显式注册的扩展名列表。

我们还可以使用consumes属性来限制content-type的类型,以缩小匹配范围:

@PostMapping(path = "/pets", consumes = "application/json") 
public void addPet(@RequestBody Pet pet) {
    // ...
}

上面代码就将匹配的范围缩小到application/json类型的pets请求。

那么,我们也可以使用produces属性来限制accept的类型:

@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8") 
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}

这里来简单说一下content-type和accept的区别:

  • content-type描述的是实体类型,也就是说它描述了所发送的数据的类型。
  • accept描述的是请求的类型,即希望接受到的数据的类型。
    那么对应到请求中,一般来说,request请求中,可以使用这两个属性来约束发送的数据类型和希望接收的数据类型,而response中,一般只有content-type。

我们也可以通过请求体或请求头的参数来干预匹配过程:

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

上述代码中,只会匹配到请求体中含有myParam属性并且值等于myValue的请求。

@GetMapping(path = "/pets", headers = "myHeader=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

同样的,也可以限制Header。

还有一种更高级的方式去干涉url的匹配,就是继承RequestMappingHandlerMapping并重写getCustomMethodCondition方法。

也可以通过编程得手段来注册处理程序,来看下官网得例子:

@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) 
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); 

        Method method = UserHandler.class.getMethod("getUser", Long.class); 

        mapping.registerMapping(info, handler, method); 
    }
}

额。。。具体效果我没有跑出来。。。惭愧~查了一些资料,似乎也没人专门用这个功能,暂时先放这把。。。此处是个坑,得填

7.Handler Method

Spring MVC支持矩阵变量。一起来看个例子:

/**
 * @Author: kuromaru
 * @Date: Created in 14:57 2019/8/5
 * @Description:
 * @modified:
 */
@RestController("webController")
@RequestMapping(value = "/mvc")
public class WebController implements InitializingBean{

    @GetMapping(value = "/hello/{id}")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String hello(@PathVariable("id") String no, @MatrixVariable("mv") String mv, HttpSession httpSession) {
        System.out.println("请求路径参数是:" + no);
        System.out.println("矩阵参数是:" + mv);
        return "hello";
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("HAHAHA");
    }
}

还需要改一下配置:

/**
 * @Author: kuromaru
 * @Date: Created in 14:52 2019/8/5
 * @Description:
 * @modified:
 */
@Configuration
public class WebMvcAdapter implements WebMvcConfigurer {

    @Bean
    public LogInterceptor logInterceptor() {
        LogInterceptor logInterceptor = new LogInterceptor();
        return logInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor()).addPathPatterns("/**");
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        // 支持矩阵参数
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

服务启动后,我们按照下面的方式来请求接口:

http://localhost:8080/mvc/hello/852;mv=789

输出结果:

请求路径参数是:852
矩阵参数是:789

可以看到,我们最终获取到了矩阵参数。
而有些时候,矩阵参数不止一个,并且参数名可能重复,如:

http://localhost:8080/mvc/calendar/kaku;no=001/2019-08-21;no=002

此时,我们可以这么写:

@GetMapping(value = "/calendar/{user}/{date}")
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
public String calendar(@PathVariable("user") String user,
                       @PathVariable("date") String date,
                       @MatrixVariable(name = "no", pathVar = "user") String no1,
                       @MatrixVariable(name = "no", pathVar = "date") String no2) {
    System.out.println("矩阵参数[no1]是:" + no1);
    System.out.println("矩阵参数[no2]是:" + no2);
    return "success";
}

输出结果:

矩阵参数[no1]是:001
矩阵参数[no2]是:002

有时候矩阵参数并不是必须的,那么就可以这么写:

    @GetMapping(value = "/pets/{type}")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String calendar(@PathVariable("type") String type,
                           @MatrixVariable(required = false, defaultValue = "kuro") String name) {
        System.out.println("矩阵参数[name]是:" + name);
        return "success";
    }

请求url

http://localhost:8080/mvc/pets/cat

输出结果:

矩阵参数[name]是:kuro

也可以一次性的把所有矩阵参数都取出来,保存在一个MultiValueMap中

    @GetMapping(value = "/pets/{type}/owners/{owner}")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String calendar(@PathVariable("type") String type,
                           @PathVariable("owner") String owner,
                           @MatrixVariable MultiValueMap<String, String> map,
                           @MatrixVariable(pathVar = "type") MultiValueMap<String, String> map2) {
        System.out.println("矩阵参数[map]是:" + map.toString());
        System.out.println("矩阵参数[map2]是:" + map2.toString());
        return "success";
    }

请求url:

http://localhost:8080/mvc/pets/cat;other=dog;third=bird/owners/neko;blood=B

输出结果:

矩阵参数[map]是:{other=[dog], third=[bird], blood=[B]}
矩阵参数[map2]是:{other=[dog], third=[bird]}

8. @RequestParam

这个用的比较多,就是用来获取请求参数的,需要注意,这个注解不仅仅可以获取到查询参数,还可以获取form的表单参数,其他的就不多说了。

9. @RequestHeader

可以通过这个注解来获取请求头的信息,如下:

    @GetMapping(value = "/owner/{owner}")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String calendar(@PathVariable("owner") String owner,
                           @RequestHeader("Accept") String[] accept,
                           @RequestHeader("Accept-Encoding") String acceptEncoding) {
        System.out.println("请求头参数[Accept]是:" + accept);
        System.out.println("请求头参数[Accept-Encoding]是:" + acceptEncoding);
        return "success";
    }

输出结果:

请求头参数[Accept]是:[Ljava.lang.String;@36666c1d
请求头参数[Accept-Encoding]是:gzip, deflate, br

留意一下accept,这个的类型是String[],打印出来Accept的原始数据是

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3

Spring内置支持使用逗号分隔成相应的数组或者集合。

10. @CookieValue

这个注解用来绑定cookie中的值,看例子:

    @GetMapping(value = "/cookie")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String calendar(@CookieValue(name = "JSESSIONID", required = false, defaultValue = "noCookie") String jsessionId) {
        System.out.println("Cookie中[JSESSIONID]是:" + jsessionId);
        return "success";
    }

输出结果:

Cookie中[JSESSIONID]是:noCookie

11. @ModelAttribute

总体来说,@ModelAttribute注解是用来把方法的参数或者方法的返回值绑定到Model Attribute上,然后公开给视图。根据@ModelAttribute所添加的位置不同,可以分为两种情况:

  • 标记在方法上
    若@ModelAttribute设置了value属性,则最终会以这个value为key,以返回值为value,存入到Model中。看代码:
    @ModelAttribute(value = "myKey")
    public String getMyKey() {
        return "my key";
    }

这个方法执行完以后,就会把“my key”添加到key是"myKey"的属性上,等同于

model.addAttribute("myKey", "my key");

如果@ModelAttribute上不指定value的话,就会根据属性的类型来生成默认key,如mypackage.OrderAddress类就会使用orderAddress作为key,List<mypackage.OrderAddress>会默认指定orderAddressList作为key。

而对于void方法来说,我们可以这么写:

@ModelAttribute
public void myMethod(Model model) {
    model.addAttribute("msg", "Hello World!");
}
  • 标记在方法参数上
    这个用法就有点类似于注入了,我们先来看段代码:
    @ModelAttribute(value = "user")
    public User prepareUser() {
        User user = new User();
        user.setName("Jackson");
        user.setNickName("Monkey");
        return user;
    }

    @GetMapping(value = "/testModelAttribute2")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String testModelAttribute(@ModelAttribute("user") User user) {
        System.out.println(user.getName());
        return "Success";
    }

当接收到testModelAttribute2请求的时候,参数user中的name和nickName就是prepareUser方法中设置的值。

输出结果:

Jackson

12. @SessionAttributes

@SessionAttributes可以把model中的数据作为session的属性存储到session中。先看段代码:

/**
 * @Author: kuromaru
 * @Date: Created in 14:57 2019/8/5
 * @Description:
 * @modified:
 */
@RestController("webController")
@RequestMapping(value = "/mvc")
@SessionAttributes("user")
public class WebController {

    @ModelAttribute(value = "user")
    public User prepareUser() {
        User user = new User();
        user.setName("Jackson");
        user.setNickName("Monkey");
        return user;
    }
    
    @GetMapping(value = "/testSessionAttributes")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String testSessionAttributes(SessionStatus status, HttpSession session) {
        System.out.println(status.isComplete());
        if (session.getAttribute("user") != null) {
            System.out.println(((User)session.getAttribute("user")).getName());
        }
        return "Success";
    }
}

输出结果:

false
Jackson

注意,这个结果是在第二次执行请求的时候才输出的,第一次不会输出Jackson,想想为什么?因为第一次请求的时候,user还没有被加入到model中,所以,也不会被加入到session中。而第二次请求的时候,model中已经有user了,因此,user才被塞入了session。

还有一点要注意,@SessionAttributes只能放在类上。除了指定model的key以外,还可以像下面这么写:

@SessionAttributes(types=User.class)会将model中所有类型为 User的属性添加到会话中。
@SessionAttributes(value={“user1”, “user2”}) 会将model中属性名为user1和user2的属性添加到会话中。
@SessionAttributes(types={User.class, Administrator.class}) 会将model中所有类型为User和Administrator的属性添加到会话中。
@SessionAttributes(value={“user1”,“user2”},types={Administrator.class})会将model中属性名为user1和user2以及类型为Administrator的属性添加到会话中。

对应的,还有一个@SessionAttribute注解,这个是用在方法参数上的,看例子:

/**
 * @Author: kuromaru
 * @Date: Created in 14:57 2019/8/5
 * @Description:
 * @modified:
 */
@RestController("webController")
@RequestMapping(value = "/mvc")
@SessionAttributes("user")
public class WebController {

    @ModelAttribute(value = "user")
    public User prepareUser() {
        User user = new User();
        user.setName("Jackson");
        user.setNickName("Monkey");
        return user;
    }
    
    @GetMapping(value = "/testSessionAttributes")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String testSessionAttributes(SessionStatus status, HttpSession session) {
        System.out.println(status.isComplete());
        if (session.getAttribute("user") != null) {
            System.out.println(((User)session.getAttribute("user")).getName());
        }
        return "Success";
    }
    
    @GetMapping(value = "/testSessionAttribute")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String testSessionAttribute(@SessionAttribute User user) {
        System.out.println(user.getNickName());
        return "Success";
    }
}

我们先执行testSessionAttributes,等user被塞入session以后,再执行testSessionAttribute,输出结果:

accept request : /mvc/testSessionAttribute
Monkey

作用很明显,跟@ModelAttribute用在方法参数上的效果类似,把session中的属性注入到请求参数中。

13. @RequestAttribute

用法跟@SessionAttribute差不多,不多说了,直接看代码:

    @ModelAttribute
    public void setRequestAttribute(HttpServletRequest request) {
        request.setAttribute("myNickName", "kaku");
    }

    @GetMapping(value = "/testRequestAttribute")
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器被外星人攻击啦~")
    public String testSessionAttribute(@RequestAttribute("myNickName") String nickName) {
        System.out.println(nickName);
        return "Success";
    }

输出结果:

accept request : /mvc/testRequestAttribute
kaku

14. redirect

可以通过下面方式设置重定向:

@PostMapping("/files/{path}")
public String upload(...) {
    // ...
    return "redirect:files/{path}";
}

在进行重定向得时候,可以使用flash Attribute来保证数据不会被重复提交,RedirectAttributes有一个addFlashAttribute方法,可以将数据存入,当执行完重定向以后,数据就会被清除。

@PostMapping("/files/{path}")
public String addCustom(@ModelAttribute("custom") Customer customer, 
    final RedirectAttribute redirectAttribute) {
    
    redirectAttribute.addFlashAttribute("message", "Success");
    return "redirect:files/{path}"
}

属性message会在重定向之后被清除。

15. Multipart

开发过程中少不了文件上传这样的需求,我们一起来看看怎么实现。

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(@RequestParam("name") String name,
            @RequestParam("file") MultipartFile file) {

        if (!file.isEmpty()) {
            byte[] bytes = file.getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}

或者

class MyForm {

    private String name;

    private MultipartFile file;

    // ...
}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        if (!form.getFile().isEmpty()) {
            byte[] bytes = form.getFile().getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}

也可以使用@RequestPart来分别获取metaData和fileData,如下:

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
        @RequestPart("file-data") MultipartFile file) {
    // ...
}

这里说一下@RequestPart,这个注解专门用来处理文件上传之类的请求,也就是multipart/form-data表单提交的请求。它支持MultipartFile在内的请求参数,是通过Spring的MultipartResolver解析,而其他的是通过HttpMessageResolver解析。@RequestParam也支持multipart/form-data请求,而与@RequestPart最大的不同在于,当参数不是String类型的时候,@RequestParam用于key-value类型的表单,而@RequestPart可用于更复杂的情况,如JSON、XML。

如果需要对参数做校验,可以在@RequestPart前面加上@Valid注解,并且通过Errors或者BindingResult来在请求处理内部处理异常。

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
        BindingResult result) {
    // ...
}

16. @RequestBody

@RequestBody通过HttpMessageConvert来解析请求参数并反序列化为对象。

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}

如果需要对参数做校验,可以在@RequestBody前面加上@Valid注解。

@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
    // ...
}

17. HttpEntity

HttpEntity与@RequestBody类似,是基于公开请求标头(key)和主体(value)的容器对象。

@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}

18. @ResponseBody

@ResponseBody通过HttpMessageConvert来把返回值序列化为实体。

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}

19. ResponseEntity

ResponseEntity与@ResponseBody类似,只不过多了status和header。

@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}

20. Jackson JSON

spring MVC为Jackson的Serialization视图提供了内置支持,它只允许渲染对象中所有字段的一个子集。要将其与@ResponseBody或ResponseEntity控制器方法一起使用,可以使用Jackson的@JsonView注释来激活序列化视图类.

@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}

对于依赖视图解析的控制器,可以将序列化视图类添加到模型中.

@Controller
public class UserController extends AbstractController {

    @GetMapping("/user")
    public String getUser(Model model) {
        model.addAttribute("user", new User("eric", "7!jd#h23"));
        model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
        return "userView";
    }
}

21. DataBinder

在@Controller和@ControllerAdvice注解的类中,可以通过@InitBinder来初始化WebDataBinder。

@Controller
public class FormController {

    @InitBinder 
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}

上边的代码逻辑即提供了一个将Date属性的值格式化的功能。如果需要对某一特定属性进行格式话,可以这么写

binder.registerCUstomEditor(Date.class, "myDate", new CustomDateEditor(dateFormat, false))

我们也可以自己继承PropertyEditorSupport,自定义处理逻辑:

public class MyDateEditor extends PropertyEditorSupport{
    @Override
    public String getAsText() {
        //获取属性值
        Date date = (Date) getValue();
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String str = dateFormat.format(date);
        String mydate =  str.substring(0,4) + "-" + str.substring(4,6) + "-" + str.substring(8,10);
        System.out.println(mydate);
        return mydate;
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        try {
            System.out.println(dateFormat.parse(text));
            //设置属性值
            setValue(dateFormat.parse(text));
        }catch (ParseException e){
            System.out.println("转换失败");
        }
    }
}
binder.registerCUstomEditor(Date.class, "myDate", new MyDateEditor());

22. Exception

@Controller和@ControllerAdvice可以使用@ExceptionHandler来处理异常。

/**
 * 全局异常处理类
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 异常处理(@RequestParam)
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ValidationException.class)
    @ResponseBody
    public ModelMap handle(ValidationException e) {
        e.printStackTrace();
        ModelMap modelMap = new ModelMap();
        modelMap.put(Constant.RESPONSE_SUCCESS, false);
        StringBuilder res = new StringBuilder();
        if (e instanceof ConstraintViolationException) {
            ConstraintViolationException exs = (ConstraintViolationException) e;
            Set<ConstraintViolation<?>> validations = exs.getConstraintViolations();
            validations.stream().forEach(item -> res.append(item.getMessage()).append(Constant.REG_LINE_NREAK).append(Constant.SPACE));
        } else {
            res.append(Constant.MSG_BAD_REQUEST);
        }
        modelMap.put(Constant.RESPONSE_CODE, ErrorCode.REQ_PARAM_VALID_DEFAULT_ERR.getCode());
        StringBuilder msgSb = new StringBuilder();
        msgSb.append("请求参数校验失败")
                .append(Constant.REG_LINE_NREAK)
                .append(Constant.SPACE)
                .append(res.toString());
        modelMap.put(Constant.RESPONSE_MSG, msgSb.toString());

        return modelMap;
    }
}

上面例子中,@ExceptionHandler传入了一个ValidationException.class作为参数,用于限定对应的异常范围,例子中可以把该参数省略。但是,如果handle的参数是一个范围较大的异常,如Exception,那么就可以通过设置@ExceptionHandler的参数来缩小范围,如:

/**
 * 全局异常处理类
 */
@ControllerAdvice
@Component
public class GlobalExceptionHandler {

    /**
     * 异常处理(@RequestParam)
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ValidationException.class)
    @ResponseBody
    public ModelMap handle(Exception e) {
        e.printStackTrace();
        ModelMap modelMap = new ModelMap();
        modelMap.put(Constant.RESPONSE_SUCCESS, false);
        StringBuilder res = new StringBuilder();
        if (e instanceof ConstraintViolationException) {
            ConstraintViolationException exs = (ConstraintViolationException) e;
            Set<ConstraintViolation<?>> validations = exs.getConstraintViolations();
            validations.stream().forEach(item -> res.append(item.getMessage()).append(Constant.REG_LINE_NREAK).append(Constant.SPACE));
        } else {
            res.append(Constant.MSG_BAD_REQUEST);
        }
        modelMap.put(Constant.RESPONSE_CODE, ErrorCode.REQ_PARAM_VALID_DEFAULT_ERR.getCode());
        StringBuilder msgSb = new StringBuilder();
        msgSb.append("请求参数校验失败")
                .append(Constant.REG_LINE_NREAK)
                .append(Constant.SPACE)
                .append(res.toString());
        modelMap.put(Constant.RESPONSE_MSG, msgSb.toString());

        return modelMap;
    }
}

如果一个handle需要匹配多个具体异常,可以这么写:

@ExceptionHandler({FileSystemException.class, RemoteException.class})

Spring对@ExceptionHandler的支持建立在HandlerExceptionResolver机制上。如果需要把错误信息写入response,可以选择扩展ResponseEntityExceptionHandler。

/**
 * @Author: kuromaru
 * @Date: Created in 10:41 2019/8/29
 * @Description:
 * @modified:
 */
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler{

    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(
            NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();

        ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
        return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
    }
}

23. ControllerAdvice

如果想把诸如@ExceptionHandler、@InitBinder和@ModelAttribute之类的设置适用于全局的控制器,可以使用@ControllerAdvice注解。也可以通过参数缩小@ControllerAdvice的作用范围:

@ControllerAdvice("org.example.MyController")
public class MyControllerAdvice {}

全局的@ExceptionHandler方法在本地的@ExceptionHandler方法之后被应用,而全局的@ModelAttribute和@InitBinder则在本地方法之前被应用。

posted @ 2020-05-22 10:20  kuromaru  阅读(344)  评论(0)    收藏  举报