• MVC 框架


    在 spring 框架和 Spring Boot 中,最常用的技术就是 MVC 框架,MVC 框架会处理类似如下相同技术的需求:

    1、HTTP UTL 映射到 Controller 某个方法;
    2、HTTP 参数映射到 Controller 方法的参数上,比如参数映射到某个Java对象,或者上传附件映射到某个File对象上;
    3、参数的校验;
    4、MVC 错误处理;
    5、MVC 中如何调用视图;
    6、MVC 中如何序列化对象成JSON;
    7、拦截器高级定制;

    引入依赖
    Spring Boot 集成 Spring MVC 框架并实现自动配置,只需要在 pom 中添加以下依赖即可,不需要其他任何配置。

    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    

    Web 应用目录结构
    Web的模板文件位于 resources/templates 目录下,模板文件使用的静态资源文件,如 js css 图片,存放在 resources/statis 目录下。在 MVC 中,视图名自动在 templates 目录下找到对应的模板名称,模板中使用的静态资源将在 static 目录下查找。

    Java 包名结构
    通常会创建如下子包名:

    Controller ---
    Service ---
    entity ---
    conf ---

    使用 Controller

    Spring MVC 框架不像传统的 MVC 框架那样必须继承某个基础类才能处理用户的 HTTP 请求, Spring MVC 只需要在类上声明 @Controller,标注这是一个 Controller 即可。对于用户请求,使用 @RequestMapping 映射 HTTP 请求到特定的方法处理类。

    @Controller
    @RequestMapping("/test)
    public class HelloworldController {
        @RequestMapping("/index.html")
        public String say(Model model) {
            model.addAttribute("name","hello world");
            return "/index.html";
        }
    }
    

    如以上代码所示,@Controller 作用于类上,表示这是一个 MVC 中的 Controller。
    @RequestMapping 既可以作用于方法上,也可以作用在类上。如上所示,用户如果访问 /test/index.html ,则会交给 HelloworldController.say 方法来处理。

    say 方法有一个参数 Model,这是 Spring MVC 可识别的一个参数类型,用来表示 MVC 中的Model。可以在这个 Model 添加多个变量,这些变量随后可以用于视图渲染。

    say 方法返回的类型是字符串,默认是视图名称。Spring Boot 的视图默认保存在 resources/templates目录下,因此,渲染的视图是 /resources/templates/index.html 模板文件。

    MVC 框架有时候返回的是 JSON 字符串,如果想直接返回内容而不是视图名,则需要在方法上使用 @ResponseBody:

    @RequestMapping("/index.json")
    public @ResponseBody String say() {
        return "hello world";
    }
    

    ResponseBody 注解直接将返回的对象输出到客户端,如果是字符串,则直接返回;如果不是,则默认使用 Jackson 序列化成 JSON 字符串后输出。

    建议在 Spring Boot 应用中,如果期望返回 JSON, URL 请求后资源后缀是 json;如果期望返回视图,URL 请求资源后缀是 html

    URL 映射到方法

    RequestMapping

    可以使用 @RequestMapping 来映射 URL,比如 /test 到某个 Controller 类,或者是某个具体的方法,通常类上的注解 @RequestMapping 用来标注请求的路径,方法上的 @RequestMapping 注解进一步映射特定的 URL 到某个具体的处理方法。

    RequestMapping 有多个属性来进一步匹配 HTTP 请求到 Controller 方法,分别是:

    value, 请求的 URL 路径,支持 URL 模板、正则表达式。
    method,HTTP 请求方法,有 GET、POST、PUT 等。
    consumes,允许的媒体类型(Media Types),如 consumes = "application/json", 对应于请求的 HTTP 的 Content-Type。
    produces, 相应的媒体类型,如 produces = "application/json",对应于 HTTP 的 Accept 字段。
    params,请求参数,如 params="action=update"
    headers, 请求的 HTTP 头的值,如 headers = "myHeader=myValue"

    URL 路径匹配

    属性 value 用于匹配一个 URL 映射,value 支持简单的表达式类匹配:

    @RequestMapping(value="/get/{id}.json")
    public @ResponseBody User getById(@PathVariable("id") Long id) {
        return userService.getUserById(id);
    }
    

    上边访问路径是 /get/1.json, 将调用 getById 方法,且参数 id 的值是 1 。注解 PathVariable 作用在方法参数上,用来表示参数的值来自于 URL 路径。

    HTTP method 匹配

    @RequestMapping 提供 method 属性来映射对应 HTTP 的请求方法,通常 HTTP 请求方法有以下内容:

    GET, 用来获取 URL 对应的内容
    POST,用来向服务器提交信息
    HEAD,同 GET,但不返回消息体,通常用于返回 URL 对应的元信息,如过期时间等。
    PUT,同 POST,用来向服务器提交信息,但语义上更像一个更新操作。同一个数据,多次 PUT 操作,也不会导致数据发生改变。POST 在语义上更类似新增操作。
    DELETE, 删除对应的资源信息。
    PATCH 方法,类似 PUT 方法,表示信息的局部更新。

    通常对于 Web 应用,GET 和 POST 是经常使用的选项,对于 REST 接口,则会使用 PUT、DELETE 等用来从语义上进一步区分操作。

    Spring 提供了简化后的 @RequestMapping, 提供了新的注解来表示 HTTP 方法:

    @GetMapping
    @PostMapping
    @PutMapping
    @DeleteMapping
    @PatchMapping

    consumes 和 produces
    属性 consumes 意味着请求的 HTTP 头的 Content-Type 媒体类型与 consumes 的值匹配,才能调用此方法。

    @GetMapping(value="/consumes/test.json",consumes="application/json")
    @ResponseBody
    public User forjson() {
        return userService.getUserById(11);
    }
    

    这里映射指定请求的媒体类型是 application/json,因此,此方法接受一个 AJAX 请求。如果通过浏览器直接访问,则会出错,因为通过浏览器访问,通常并没有设置 Context-Type。

    为了成功调用上述Controller方法,AJAX调用必须设置 Content-Type 为 application/json

    $.ajax({
        type: "get",
        url: "/consumes/test.json",
        contentType: "application/json",
        ....
    })
    

    produces属性对应于 HTTP 请求的 Accept 字段,只有匹配的上的方法才能被调用。

    @GetMapping(value="/user/{userid}",produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public User getUser(@PathVariable Long userId, Model model) {
        return userService.getUserById(userId);
    }
    

    通常浏览器都会将 Accept 设置为 .

    params 和 header 匹配
    可以从请求参数或者 HTTP 头中提取值来进一步确定调用的方法,有以下三种形式:

    如果存在参数,则通过;
    如果不存在参数,则通过;
    如果参数等于某一个具体值,则通过。

    @PostMapping(value="/update.json",params="action=save")
    @ResponseBody
    public void saveUser() {
        System.out.println("call save");
    }
    

    header 和 params 一样:

    @PostMapping(value="/update.json",headers="action=save")
    @ResponseBody
    public void saveUser() {
        System.out.println("call save");
    }
    

    方法参数

    Spring 的 Controller 方法可以接受多种类型参数,比如我们看到的 path 变量,还有 MVC 的 Model,除此之外,方法还能接受以下参数。

    @PathVariable, 可以将URL中的值映射到方法参数中。
    Model,Spring 中通用的MVC模型,也可以使用 Map 和 ModelMap 作为渲染视图的模型。
    ModelAndView, 包含了模型和视图路径的对象。
    JavaBean,将HTTP参数映射到 JavaBean对象。
    MultipartFile,用于处理文件上传。
    @ModelAttribute 使用该注解的变量将作为 Model 的一个属性。
    WebRequest 或者 NativeWebRequest,类似 Servlet Requset ,但做了一定封装。

    PathVariable

    注解 PathVariable 用于从请求 URL 中获取参数并映射到方法参数中

    @RequestMapping(value="/get/{id}.json")
    public @ResponseBody User getById(@PathVariable("id") Long id) {
        return userService.getUserById(id);
    }
    

    Model & ModelAndView
    任何 MVC 框架都有一个类似 Map 结构的 Model,可以向 Model 添加视图需要的变量,Spring MVC 中的 Model 就是完成此功能的。Model 对象主要有如下方法:

    Model addAttribute(String attributeName, Object attributeValue),向模型添加一个变量,attributeName 指明了变量的名称,可以在随后的视图里引用,attributeValue 代表了变量。
    Model addAttribute(Object attributeValue) , 向模型添加一个变量,变量的名字就是其类名首字母小写后转为 Java 变量。

    ModelAndView 对象类似 Model,但额外提供了一个视图名称。

    javaBean 接受 HTTP 参数
    可以通过注解 @RequestParam来进一步限定 HTTP 参数到 Controller 方法的映射关系,RequestParam 支持如下属性:

    value, 指明 HTTP 参数的名称
    required, boolean 类型,声明此参数是否必须有,如果 HTTP 参数里没有,则会抛出 400 错误。
    defaultValue,字符类型,如果 HTTP 参数没有提供,可以指定一个默认字符串。

    可以将 HTTP 参数转为 JavaBean 对象, HTTP 参数的名字对应到 POJO 的属性名:

    @GetMapping(path="/update.json")
    @ResponseBody
    public String updateUser(User user) {
    return "success";
    }
    

    通常,HTTP 提交了多个参数,Spring 支持按照前缀自动映射到不同的对象上。比如用户交了订单信息,订单信息通常包含了多个订单明细。

    需要创建一个 Form 对象来接收 HTTP 提交的数据:

    public class OrderPostForm {
    private Order order;
    private List<OrderDetail> details;
    }
    

    如上所示,以“order”为前缀的HTTP参数映射到 OrderPostForm 类的 order 属性上,以 details 属性为前缀的 HTTP 参数映射到 OrderPostForm 的 details 属性上。

    <form>
    订单名称 <input name="order.name"/>
    订单明细一
    <input name="details[0].name" />
    订单明细二
    <input name="details[1].name" />
    </form>
    

    则下面的 Controller 的方法可以处理这个请求

    @PostMapping(path="")
    @ResponseBody
    public String saveOrder(OrderPostForm form) {
    return "success";
    }
    

    @RequestBody 接受 JSON
    Controller 方法带有 @RequestBody 注解的参数,意味着请求的 HTTP 消息体的内容是一个 JSON,需要转化为注解指定的参数类型。Spring Boot 默认使用 Jackson 来处理反序列化工作。

    如果客户端发起了一个 JSON 请求,则有如下定义来接受 JSON:

    @PostMapping(path="/savejsonorder.json")
    @ResponseBody
    public String saveOrderByJson(@ResquestBody User user) {
    return user.getName();
    }
    

    这段代码能够处理客户端发起的JSON请求

    >curl -XPOST 'http://127.0.0.1:8080/savejsonorder.json' -H 'Content-Type: application/json' -d '{
    "name": "Hello",
    "id":1
    }'
    

    MultipartFile
    通过 MultipartFile来处理文件上传:

    @PostMapping("/form")
    @ResponseBody
    public String handleFormUpload(String name, MultipartFile file) throws IOException {
    if(!file.isEmpty()) {
    String fileName = file.getOriginalFilename();
    InputStream ins = file.getInputStream();
    return "success";
    }
    return "failure";
    }
    

    MultipartFile 提供了一下方法来获取上传文件的信息。

    1. getOriginalFilename 获取上传文件的名字。
    2. getBytes 获取上传文件内容,转为字节数组。
    3. getInputStream 获取一个 InputStream
    4. isEmpty 上传文件内容为空,或者就没有文件上传
    5. getSize 文件上传的大小
    6. transferTo(File dest)保存上传文件到目标文件系统。

    如果是同时上传多个文件,则使用 MultipartFile 数组来接受多个文件上传:

    @PostMapping("/form")
    @ResponseBody
    public String handleFormUpload(String name, MultipartFile[] files) throws IOException {
    
    }
    

    这要求自HTTP请求中包含多个名为 "files" 的文件:

    <form action="filesUpload.html" method="post" entype="multipart/form-data">
    1
    <input  type ="file" name="files" />
    2
    <input  type ="file" name="files" />
    3
    <input  type ="file" name="files" />
    4
    <input  type ="file" name="files" />
    
    </form>
    

    可以通过配置文件对Spring Boot 上传的文件进行限定,默认如下:

    spring.servlet.multipart.enabled=true
    spring.servlet.multipart.file-size-threshold=0
    spring.servlet.multipart.location=
    spring.servlet.multipart.max-file-size=1MB
    spring.servlet.multipart.max-request-size=10MB
    spring.servlet.multipart.resolve-lazily=false
    

    参数 enabled 默认为 true,即允许附件上传。
    file-size-threshold 限定了当上传的文件超过一定长度时,就先先写到临时文件里。这有助于上传文件不占用过多内存,单位时 MB 或者 KB。默认是0,不限定阈值。
    location 值指的是临时文件的存放目录,如果不限定,则是 Web 服务器提供的一个临时目录。
    max-file-size 属性指定了单个文件的最大长度,默认是 1 MB
    max-request-size 属性说明单次 HTTP 请求上传的最大长度,默认 10 M
    @ModelAttribute
    注解 ModelAttribute 通常作用在 Controller 的某个方法上,此方法会首先被调用,并将方法结果作为 Model的属性,然后再调用对应的 Controller 处理方法。

    @ModelAttribute
    public void findUserById(@PathVariable Long id, Model model) {
    mode.addAttribute("user",userService,getUserById(id));
    }
    
    @GetMapping(path="/{id}/get.json")
    @ResponseBody
    public String getUser(Model model) {
    System.out.println(model.containsAttribute("user"));
    return "success";
    }
    

    对于 HTTP 请求,modelattribute/1/get.json ,会调用 findUserById 方法,取得 user, 并添加到模型里。使用 ModelAttribute 通常可以用来向一个 Controller 中需要的公共模型添加数据。

    验证框架
    Spring Boot 支持 JSR-303、Bean 验证框架,默认实现使用的是 Hibernate validator。在 Spring MVC 中,只需要使用 @Valid 注解在方法参数上,Spring Boot 即可对参数对象进行校验,校验结果放在 BindingResult 对象中。

    JSR-303
    jsr-303 是 Java 标准的验证框架,已有的实现有 Hibernate validator。JSR-303 定义了一些列注解来验证Bean 的属性,常用的有如下几种。

    空检查
    1、 @Null,验证对象是否为空
    2、 @NotNull,验证对象不为空
    3、 @NotBlank,验证字符串不为空或者不是空字符串
    4、 @NotEmpty, 验证对象不为 null,或者集合不为空

    长度检查
    1、 @Siz(min= , max=), 验证对象长度,可支持字符串、集合;
    2、 @Length, 字符串大小
    数值检测
    1、 @Min,验证数字是否大于指定值
    2、 @Max, 验证数字是否小于等于指定的值
    3、 @Digits, 验证数字是否符合指定格式,如 @Digits(integer=9,faraction=2)
    4、 @Range, 验证数字是否在指定范围内,如 @Range(min=1,max=100)
    其他
    1、 @Email, 验证是否为邮件格式,为 null 则不做校验
    2、 @Pattern, 验证String对象是否符合正则表达式的规则

    public class WorkInfoForm {
    @NotNull
    Long id;
    @Size (min=3,max=20)
    String name;
    @Email
    String email;
    }
    

    通常不同的业务逻辑会有不同的验证逻辑,比如对于 WorkInfoForm来说,当更新的时候, id 必须不为null,但增加的时候, id必须是null。

    JSR-303 定义了group概念,每个校验注解都必须支持。校验注解作用在字段上的时候,可以指定一个或者多个group,当Spring Boot校验对象的时候,也可以指定校验的上下文属于那个group。这样,只有group匹配的时候,校验注解才能生效。上面的WorkInfoForm定义id字段校验可以更改为如下内容:

    public class WorkInfoForm {
    
    // 定义一个类,更新时校验组
    public interface Update{}
    //定义一个类,添加时校验组
    public interface Add{}
    
    @NotNull(groups={Update.class})
    @Null(groups={Add.class})
    Long id;
    @Size (min=3,max=20)
    String name;
    @Email
    String email;
    }
    

    这段代码表示,当校验上下文为 Add.class 的时候,@Null 生效,id需为空才能校验通过;当校验上下文为 Update.class 的时候,@NotNull 生效,id不能为空。

    MVC 中使用 @Validated
    在Controller中,只需要给方法参数添加上@Validated即可触发一次校验。

    @ResponseBody
    @RequestMapping("/addworkinfo.html")
    public void addWorkInfo(@Validated({WorkInfoForm.Add.class}) WorkInfoForm workInfo, BindingResult result) {
    if(result.hasErrors()) {
    List<ObjectError> list = result.getAllErrors();
    FieldError error = (FieldError) list.get(0);
    System.out.println(error.getObjectName()+"," + error.getField() + "," + error.getDefaultMessage());
    }
    }
    

    此方法可以接受 HTTP 参数并映射到 WorkInfoForm对象,WorkInfoForm使用了@Validated注解,将触发Spring的校验,并将验证结果存放在BindingResult对象中。这里,Validated 注解使用了校验的上下文 WorkInfoForm.Add.class 因此,整个校验将按照 Add.class 来校验。

    BindingResult 包含了验证结果,提供了如下方法。

    hasErrors 判断验证是否通过。
    getAllError 得到所有的错误信息,通常返回的是 FieldError 列表

    如果 Controller 参数未提供 BindingReult 对象,则 SpringMVC 将抛出异常。

    自定义校验
    JSR-303 提供的大部分校验注解已经够用,也允许定制校验注解,比如在 WorkInfoForm 类中,我们新增加一个加班时间。

    @WorkOverTime(max=2)
    int workTime;
    

    属性 workTime 使用了注解 @WorkOverTime,属性值超过 max 值的时候,将会验证失败。WorkOverTime 跟其他注解差不多,但提供了 @Constraint 来说明用什么类作为验证注解实现类。

    @Constraint(validatedBy= {WorkOverTimeValidator.class})
    @Documented
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WorkOverTime {
    String message() default "加班时间过长,不能超过{max} 小时";
    int max() default 5;
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    }
    

    @Constraint注解声明用什么类来实现验证,我们将创建一个 WorkOverTimeValidator 来进行验证。验证注解必须要提供如下信息。

    message 用于创建错误信息
    groups 验证规则分组,比如新增和修改的验证规则不一样,分为两个组,验证注解必须提供
    payload 定义了验证的有效负荷

    WorkOverTimeValidator 必须实现 ConstraintValidator 接口 initialize 方法及验证方法 isValid;

    public class WorkOverTimeValidator implements ConstraintValidator<WorkOverTime, Integer> {
    WorkOverTime work;
    int max;
    public void initialize(WorkOverTime work) {
    thie.work = work;
    max = work.max();
    }
    
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
    if(value == null) {
    return true;
    }
    return value<max>;
    }
    }
    

    WebMvcConfigurer
    WebMvcConfigurer 是用来全局制定 Spring Boot 的 mvc 特性。开发者通过实现 WebMvcConfigurer 接口来配置应用的 MVC 全局特性。

    @Configuration
    public class MvcConfigurer implements WebMvcConfigurer {
    //拦截器
    public void addInterceptors (InterceptorRegistry registry) {
    
    }
    
    // 跨域访问配置
    public void addCorsMappings(CorsRegistry registry) {
    
    }
    
    //格式化
    public void addFormatters (FormatterRegistry registry) {
    
    }
    
    // URI 到视图的映射
    public void addViewControllers(ViewControllerRegistry registry) {
    
    }
    
    }
    

    拦截器
    通过 addInterceptors方法可以设置多个拦截器,比如对特定的 URI 设定拦截器以检查用户是否登录,打印处理用户请求耗费的时间等。

    public void addInterceptors(InterceptorRegistry registry) {
    // 增加一个拦截器,检查会话, URL 以 admin 开头的都适用此拦截器
    registry.addInterceptor(new SessionHandlerInterceptor()).addPathPatterns("/admin/**");
    }
    
    class SessionHandlerInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    User user = (User) requset.getSession().getAttribute("user");
    if(user == null) {
    //没有登录,重定向到 login.html
    response.sendRedirect("/login.html");
    return false;
    }
    return true;
    }
    
    publica void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    // controller 方法处理完毕后,待用此方法
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) throws Exception {
    // 页面渲染完毕后调用此方法,通常用来接触某些资源。
    }
    }
    

    上边这段代码,我们在 MvcConfigurer 总实现了 addInterceptors 方法,并对访问地址 /admin/** 添加了一个拦截器。

    拦截器,有以下三个方法需要覆盖实现

    preHandle,在调用 Controller 方法前会调用此方法
    postHandle,在调用Controller 方法结束后、页面渲染之前调用此方法,比如可以在这里将渲染的视图名称更改为其他视图名
    afterCompletion ,页面渲染完毕后调用此方法。
    跨域访问
    处于安全的考虑,浏览器会禁止 AJAX 访问不同域的地址。
    Spring Boot 提供了对 CORS 的支持,可以实现 addCorsMappings 接口来添加特定的配置:

    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**");
    }
    

    允许所有跨域访问,或者更为精细的控制

    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/api/**")
    .allowedOrigins("http://doain2.com")
    .allowedMethods("POST","GET");
    }
    

    跨域原理简单理解就是发起跨域请求的时候,浏览器会对请求域返回的响应信息检查HTTP头,如果Access-Control-Allow-Origin 包含了自身域,则表示允许访问。否则报错,这就是 allowedOrigins 的作用。

    注册Controller
    应用有时候没有必要为一个 URL 制定一个Controller方法,可以直接将 URI 请求转到对应的模版渲染上。可以直接通过 ViewControllerRegistry 注册一个:

    public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/index.html").setViewName("/index.html");
    registry.addRedirectViewController("/**/*.do","/index.html");
    }
    

    视图技术
    Redirect 和 Forward

    Contoller 中重定向可以返回以 "redirect:" 为前缀的 URL

    @RequestMapping("/order/saveorder.html")
    public String saveOrder(Order order) {
    Long orderId = service.addOrder(order);
    return "redirect:/order/detail.html?orderId=" + orderId;
    }
    

    或者直接使用 RedirectView

    RedirectView view = new RedirectView("/order/detail.html?orderid=" + orderId);
    

    Spring MVC 也支持 foward前缀,用来在 Controller 执行完毕之后,再执行另外一个 Controller 的方法。

    通用错误处理
    在 Spring Boot 中, Controller 中抛出的异常默认交给了 /error 来处理,应用程序可以将 /error 映射到一个特定的 Controller 中处理来代替 Spring Boot 的默认实现,应用可以继承 AbstractErrorController 来统一处理系统的各种异常。

    @Controller
    public class ErrorController extends AbstractErrorController {
    Log log = LogFactory.getLog(ErrorController.class);
    
    @Autowired
    ObjectMapper objectMapper;
    
    public ErrorController() {
    super(new DefaultErrorAttributes());
    }
    
    @RequestMapping("/error")
    public ModelAndView getErrorPath(HttpServletRequest request, HttpServletResponse response) {
    //处理异常
    
    }
    }
    

    AbstractErrorController 提供了多个方法可以从 request 中获取错误信息,包含以下信息:

    timestamp, 错误发生的时间
    status 对应于 HTTP status 如 404
    error, 错误消息,如 Bad Requset、Not Found
    message, 详细错误信息
    exception, 如果应用抛出异常
    path,请求的 URI
    errors, @Validated 校验错误的时候,校验结果信息放在这里。

    考虑到异常信息直接显示给应用系统客户并不合适,尤其是 RuntimeException。同时,还要区分页面渲染和JSON 请求这两种不同的情况,前者应该返回一个错误页面,后者应该返回一个 JSON 结果。因此,为应用提供的统一错误处理代码:

    @RequestMapping(ERROR_PATH)
    public ModelAndView getErrorPath (HttpServletRequest request, HttpServletResponse response) {
    Map<String,Object> model = Collections.unmodifiableMap(getErrorAttributes(request, false));
    // 获取异常,有可能为空
    
    Throwable cause = getCause(request);
    int status = (Interger) model.get("status");
    
    // 错误信息
    String message = (String) model.get("message");
    
    // 提示
    String errorMessage = getErrorMessage(cause);
    //后台打印日志信息
    log.info(status+"," + message , cause);
    response.setStatus(status);
    if(!isJsonRequest(requst)) {
    //error.html
    ModelAndView view = new ModelAndView("/error.html");
    view.addAllObjects(model);
    view.addObject("errorMessage",errorMessage);
    view.addObject("status",status)
    view.addObject("cause",cause);
    return view;
    }else {
    Map error = new HashMap();
    error.put("success",false);
    error.put("errorMessage",errorMessage);
    error.put("message",message);
    writeJson(response,error);
    return null;
    
    }
    
    }
    

    getErrorAttributes 方法是 AbstractErrorController 提供的用于获取错误信息的方法,返回一个 Map,包含的数据如下。

    getCause 方法用于获取应用系统的异常,定义如下:

    protected Throwable getCause(HttpServletRequest request) {
    Throwable  error = (Throwable) request.getAttribute("javax.servlet.error.exception");
    if(error != null) {
    // MVC 有可能会封装异常成 ServletException 需要调用 getCause 获取真正的异常 
    while( error instanceof ServletException && error.getCause() != null) {
    error = ((ServletException ) error).getCause();
    }
    }
    return error;
     }
    

    getErrorMessage 方法返回一个友好的异常信息,而不是 Spring Boot 提供的 message 包含的信息:

    protected String getErrorMessage(Throwable ex) {
    return "服务器错误,请联系管理员";
    }
    

    通常这个友好信息很简单,类似上边。也可以根据应用系统自定义的异常进一步输出详细信息

    protected String getErrorMessage(Throwable ex) {
    if(ex instanceof YourApplicationException) {
    return ((YourApplicationException) ex).getMessage();
    }
    
    return "服务器错误,请联系管理员";
    }
    

    isJsonRequest 方法用来区分客户端发起的是页面渲染请求还是 JSON 请求:

    protected boolean isJsonRequest(HttpServletRequest request) {
    String requestUri = (String) request.getAttribute("javax.servlet.error.requeset_uri");
    if(requestUri != null && requestUri.endsWith(".json")) {
    return true;
    }else {
    //也可以通过获取 HTTP 头,根据 Accept 字段是否包含 JSON 来进一步判断
    // request.getHeader("Accept").contains("application.json");
    return false;
    }
    
    }
    

    @Service 和 @Transactional
    到目前为止,Spring Boot 的 Controller 介绍完了,在 Spring Boot 中, Controller 调用业务逻辑处理交给了被 @Service 注解的类,这也是一个普通的 JavaBean,Controller 中可以自动注入这种 Bean,并调用其方法完成主要的业务逻辑。正如 Controller 注解经常和 @RequestMapping 搭配使用一样,@Service@Transactional 配合使用。

    声明一个 Service 类

    publica interface UserService {
    public User getUserById(Long id);
    public void updateUser(Long id, Integer type);
    }
    

    然后实现此业务接口,不要忘记增加@Service 来引起 Spring Boot 的注意,同时搭配上 @Transactional, Spring Boot 会对这样的 Bean 进行事物增强。

    @Service
    @Transactional // 也可以作用于放上上
    public class UserServiceImpl implements UserService {
    public User getUserById(Long id){
    return user
    }
    public void updateUser(Long id, Integer type) {
    
    }
    }
    

    当 Controller 调用 Service 方法的时候,会开启一个事物上下文,随后的调用都将处于这个事物上下文中。如果调用这个 Service 方法抛出 RuntimeException , 事物则会自动回滚。

  • 相关阅读:
    关于JQ中的extend及扩展
    获取javabean 属性,类型,值
    Go调度器系列(3)图解调度原理
    grpcgateway使用教程
    MySQL 自增主键为啥不是连续递增
    Go调度器系列(2)宏观看调度器
    golang中defer,panic,recover的用法
    vue项目通过nginx部署在子目录
    Go调度器系列(1)起源
    Golang GPM 模型剖析
  • 原文地址:https://www.cnblogs.com/dowhile/p/MVC-kuang-jia.html
Copyright © 2020-2023  润新知