Spring Web MVC

1 基本配置

在 Web.xml 中配置前端控制器,让 Spring MVC 拦截处理所有的请求:

<servlet>
  <servlet-name>web</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/web-mvc.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>web</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

Spring MVC 在启动的时候会初始化容器,所以需要通过 xml 配置其容器的初始化。Spring-Mvc.xml:

<!--启用注解扫描-->
<context:component-scan base-package="com.nf147.post.controller" />

<!--启用 mvc 的常用注解-->
<mvc:annotation-driven enable-matrix-variables="true" />

<!--将所有的静态资源交还 Servlet 处理-->
<mvc:default-servlet-handler />

<!--配置返回页面-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/jsp/" />
  <property name="suffix" value=".jsp" />
</bean>

2 DispatcherServlet

前端控制器,所有来自客户端的请求,都会交由它去处理。

3 Controller

3.1 Hello, World

传统方式,使用 Controller 接口:

// 1. 实现 Controller 接口,当然也可以直接使用其内置的实现类
// 2. 重写 handleRequest 方法
// 注意,方法的返回值是 ModelAndView
public class FooController implements Controller {
    @Override
    public ModelAndView handlerRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView("home", "msg", "hello, this is from Controller interface"); // 返回 home 页面,携带 ${msg} 数据
    }
}
// <bean name="/foo" class="imdemo.controllers.FooController" />
// curl http://localhost:8888/foo

注解方式,可让定义变得简单:

@Controller
public class FooController {
    @RequestMapping("/a")
    public ModelAndView aaa() {
        return new ModelAndView("home", "msg", "hello, this is from Controller interface");
    }
    @RequestMapping("/b")
    public String bbb(Model model) {
        model.addAttribute("msg", "hello, this is from Controller interface");
        return "home";
    }
}
// 不需要实现接口,简单,解耦合;一个 Controller 类可以有多个 handler
// 另注意,需要在配置中启用 Component-Scan

3.2 RequestMapping

使用 @RequestMapping 来为 handler 绑定映射:

@RequestMapping(value='/foo', method='GET‘, headers=, params=,)

示例:

@Controller
public class BookController {
    @Autowired private BookService bookService;

    @RequestMapping(value="/books")
    public ModelAndView books () {
        return new ModelAndView("home", "books", bookService.findAll());
    }

    @RequestMapping(value = "/book/{publishDate}", method = RequestMethod.GET)
    public String getId(@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date publishDate, Model model) {
        model.addAttribute("books", bookService.findByPublishDate(publishDate));
        return "home";
    }

    @PostMapping(value = "/book")
    public String insert(@Valid Book book, BindingResult result, RedirectAttributes attributes) {
        if (result.hasErrors()) {
            return "input";
        }
        attributes.addAttribute("book", bookService.createBook(book));
        return "redirect: /books";
    }

    @DeleteMapping(value = "/book/{id}")
    public String delete(int id) {
        bookService.deleteById(id);
        return "home";
    }

    @GetMapping("/book/{p1}/{p2}")
    @ResponseBody
    public String bookPV(@PathVariable int p1, @PathVariable int p2, ModelMap map) {
        return String.valueOf(p1 + p2);
    }

    @GetMapping(value = "/book/{id:\\d{6}}-{name:[a-z]{3}}")
    public String bookWildcard(@PathVariable int id, @PathVariable String name, Model model) {
        model.addAttribute("message", "id: " + id + " name:" + name);
        return "debug";
    }

    // 注意,此功能需要在配置文件中启用
    @GetMapping(value = "/books/{id}") // GET /books/42;q=11;r=22
    public void findBook(@PathVariable String id, @MatrixVariable int q) {
        // id == 42
        // q == 11
    }
}
// @GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@PatchMapping

另外,可以使用 @RequestMapping 的参数对请求、响应进行控制:

// 请求内容类型必须为 text/html
@RequestMapping(value="/aaa", consumes="text/html");
// 客户端接收 json 且编码为 utf-8
@RequestMapping(value="/bbb", produces="application/json; charset=UTF-8");
// 请求的参数必须包含 id=215 与 name 不等于 abc
@RequestMapping(value="/ccc", params={"id=215", "name!=abc"});
// 请求头部信息中必须包含 Host=localhost:8888
@RequestMapping(value="/ddd", headers="Host=localhost:8888");

本身浏览器支持 GET/POST 方法,为了能使用 DELETE 等请求,需要配置 web.xml:

<filter>
  <filter-name>hmf</filter-name>
  <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>hmf</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

然后在 form 表单中添加一个隐藏域 _method="delete",就可以通过 post 方法模拟 delete 请求了。 也可以通过 form:form 标签简化此操作。

当然,ajax 提交自身支持各种 http 方法,就不需要这个过滤器了。

3.3 Handler 参数

Spring 会根据 handler 参数的类型签名创建并注入相应实例:

  • ServletRequest/ServletResponse/HttpSession/InputStream/OutputStream / Locale/TimeZone/Principal
  • WebRequest/NativeWebRequest/HttpMethod/HttpEntity/UriComponentsBuilder/MultipartFile
  • Map/Model/ModelMap/RedirectAttributes / Errors/BindingResult

对于添加了相关注解的参数,会按照定义进行相关的 Data Binding:

  • @RequestHeader/@CookieValue/@RequestPart
  • @ModelAttribute/@SessionAttribute/@RequestAttribute
  • @RequestParam/@PathVariable/@MatrixVariable

如果参数跟上述的任何一种都不匹配,那么它会根据 BeanUtils#isSimpleProperty 判断:

  • 如果是简单类型,那么它会被解析为 @RequestParam
  • 如果是复杂类型,那么它会被解析为 @ModelAttribute

数据绑定的基本规则,示例如下:

类型 描述 示例 测试
基本类型 按名字进行匹配 handler(int id, String title) GET /x?id=3&title=hello
普通对象 通过反射赋值 handler(Post post) GET /x?id=3&title=hello
复杂对象 对象里包含对象 handler(Post post) GET /x?id=3&author.name=luxun
List集合 使用 [n] handler(FormBean posts) POST /x?y[0].id=3&y[1].title=hello
Map集合 使用 [key] handler(FormBean posts) POST /x?y[i].id=3&y[i].title=hello

@ModelAttribute:

可以把注解 @ModelAttribute 作用于函数的参数(或者函数上),将其强制转换为 ModelAttribute:

  • 如果在 Model 中已经存在此实例,直接使用
  • 如果在 Model 中没有此实例,那么先在 Model 里创建一个新的,再使用

如果将 @ModelAttribute 放置到方法上:

  • 这个方法将会在 Controller 的任何 handler 调用前都会被执行。[复用]
  • 方法的返回结果,将会被放入到 Model 中。[可用来预备数据]

3.4 Handler 返回

主要的返回方式有:

  • ModelAndView/Model/Map/View/String/void / DeferredResult/Callable/ListenableFuture
  • @ResponseBody/@ResponseStatus/ResponseEntity / ResponseBodyEmitter/SseEmitter/StreamingResponseBody

Spring 提供了两种方法将资源转换为发送给客户端的表述形式:

  1. Content Negotiation,选择一个视图,将模型渲染为呈现给客户端的表述形式;
  2. Message Conversion,通过一个消息转换器将返回的对象转换为呈现给客户端的表述形式

默认情况下,使用的是第一种方式进行结果渲染:

  • 如果不指定视图,那么将会用请求 url 作为默认视图名
  • 返回结果都会统一被合并为一个 ModelAndView 对象,之后通过 viewResolver 进行选择性渲染

重定向与转发:

return new RedirectView("xxx");
return "redirect: /xxx";
return "forward: /xxx";

重定向后,如果想让一些数据在下一次请求中有效,则需要使用 RedirectAttributes:

@RequestMapping("/")
public String aaa (RedirectAttributes attributes) {
    // 将会使用 url 重写方式。在下一个页面中,使用 ${param.msg} 访问
    attributes.addAttribute("msg", "url parameter");

    // 将会将数据保存在 session 中,下一次请求后清除掉,使用 ${gsm} 访问
    // 注意,这种方式,需要转发到一个新的 handler 请求,不能是一个 jsp
    attributes.addFlashAttribute("gsm", "session parameter");

    return "redirect: /xxx";
}

如果想使用第二种方式,即直接响应内容而非渲染视图,手段有很多:

  1. 在 handler 上添加 @ResponseBody 注解
  2. 在 Controller 上添加 @RestController 注解
  3. 让 handler 直接返回一个 HttpEntity 对象
  4. 当然,如果在 handler 里调用了输出流,也可以导致第一种方式失效

然后,MessageConversion 会根据请求的 Accept 头以及路径中的 jar 包,选择合适的转换器对数据进行转换,比如 MappingJacksonHttpMessageConverter

可以使用这种方式,结合 ajax+json 实现 RESTful 风格的编程,实现前后端的分离。

3.5 reqeust data bind to Collection

3.5.1 via FormBean

客户端:

<form action="/book" method="post">
  <div>
    <input name="books[0].name">
    <input name="books[0].price">
  </div>
  <div>
    <input name="books[1].name">
    <input name="books[1].price">
  </div>
  <input type="submit">
</form>

服务端:

@GetMapping(value = "/book")
public String input() {
    return "input";
}

@PostMapping(value = "/book")
public String add(FormBean fb) {
    return "home";
}

必须一个 FormBean 作为中间数据载体,FormBean 又称 VO

3.5.2 via FormBean with Validation

如果想要捕获错误,并回显,需要改造。

客户端:

<form:form action="/book" method="post" modelAttribute="formBean">
  <div>
    <form:input path="books[0].name" />
    <form:input path="books[0].price" />
    <form:errors path="books[0]*" element="footer" />
  </div>
  <div>
    <form:input path="books[1].name" />
    <form:input path="books[1].price" />
    <form:errors path="books[1]*" element="footer" />
  </div>
  <input type="submit">
</form:form>

服务端:

@GetMapping(value = "/book")
public String input(FormBean fb) {
    return "input";
}

@PostMapping(value = "/book")
public String add(FormBean fb, BindingResult result) {
    return result.hasErrors() ? "input" : "home";
}

3.5.3 via Ajax and RequestBody

客户端:

fetch('/book', {
    method:'POST',
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify([{"name": "peace", "price": "32"},{"name": "kkkk", "price": "21"}])
}).then(resp => resp.text()).then(console.log).catch(console.error);

服务端:

@ResponseBody
@PostMapping("/book")
public int add(@RequestBody List<Book> books) {
    dao.save(books);
    return 1;
}

注意,这种方式,不能使用 BindingResult 的方式捕获异常(因为不是 Modelattribute..),所以,只能靠捕获异常,比如捕获 HttpMessageNotReadableException 等。

4 DataBinding/Conversion (类型转换)

三种方式:

  1. PropertyEditor
  2. Convertor
  3. Formatter

日期转换为例。

4.1 第一种方法:利用内置的 CustomDateEditor

首先,在我们的 Controller 的 InitBinder 里面,注册 CustomEditor

@InitBinder
public void init (WebDataBinder binder) {
    CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true);
    binder.registerCustomEditor(Date.class, dateEditor);
}

然后,就可以正常转换了。

4.2 第二种方法:实现自定义 Converter

定义:

public class MyDateConverter implements Converter<String, Date> {
    public Date convert(String datestr) {
        // 只是示例,实际要考虑更多,比如异常处理等
        return new SimpleDateFormat("yyyy-MM-dd").parse("2011-10-23");
    }
}

然后配置并注册 ConversionService:

<bean name="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
  <property name="converters">
    <set>
      <bean class="imdemo.converter.MyDateConverter" />
    </set>
  </property>
</bean>

<mvc:annotation-driven conversion-service="conversionService" />

这样就可以了。所有的 yyyy-MM-yy 之类的字符串就可以正常自动转换成 Date 对象了。

4.3 第三种方法:使用 @DateTimeFormat 注解

在 model 上,增加相应注解:

class Book {
  @DateTimeFormat(pattern = "yyyy-MM-dd")
  private Date created_at;
}

就可以了。

另外,如果想让返回的 JSON 对象中能够准确处理时间类型,需要用到 @JsonFormat 注解

Spring 还内置了一些 Formatter 实现:

  • NumberFormatter,处理数字类型(比如 1, 000, 1000 格式的数据)
  • PercentFormatter,处理百分号数字
  • CurrencyFormatter,处理货币类型

5 DataBinding/Validation (验证)

5.1 JSR-303

JSR-303 是 java 官方推出的一套 Validation 接口。

hibernate 给出了一个完整实现:

complie "org.hibernate:hibernate-validator:5.4.0.Final"

引入 jar 包后,添加验证逻辑,它使用的是一系列注解:

public class book {
    @notnull
    @size(min = 3, max = 10)
    private string name;

    @range(min = 10, max = 100)
    private int count;
}

最后,只要在 Controller 的相关字段上添加 @Valid 注解即可启用验证:

public String submit(@Valid Book book, Errors errors) {
    if (errors.hasErrors()) {
        return "input";
    }
    System.out.println("normal flow");
    return "home";
}

Bean Validation 中内置的 constraint:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits(integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator 附加的 constraint:

  • @NotBlank 验证字符串非null,且长度必须大于0
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内

另,自定义 JSR303 验证器,只需要:

  1. 定义验证注解
  2. 增加验证器(ConstraintValidator)
  3. 正常使用

5.2 JSR303 例子

5.2.1 一个订单类

public class Order {

    // 必须是 10 位
    @NotBlank
    @Size(min = 10, max = 10)
    private String orderId;

    @NotBlank
    private String customer;

    @Email
    private String email;

    @Pattern(regexp = "^[0-9]{11}$") //?
    private String telephone;

    @NotBlank
    private String address;

    // created paid shipped closed
    @NotEmpty
    @Pattern(regexp = "^(created|paid|shipped|closed)$")
    private String status;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @NotNull
    @Past
    private Date createDate;

    @NotNull
    @Valid
    private Product product;
}

public class Product {
    @NotBlank
    private String name;

    //@Range(max = 100000, min = 10)
    @Min(100)
    @Max(10000)
    private Float price;

    public String getName() {
        return name;
    }
}

///////////////////////////////////////

@Controller
public class OrderController {
    @RequestMapping(value = "/order", method = RequestMethod.POST)
    @ResponseBody
    public Order order (@Valid @RequestBody Order order, BindingResult result) {
        return order;
    }
}

客户端调用测试:

fetch('/order', {
    method: 'post',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        orderId: '1234567888',
        customer: 'tom',
        telephone: '10000000000',
        address: 'hello, zhuhai',
        email: '232@ksdjfk.com',
        createDate: '2014-12-11',
        status: 'paid',
        product: {
            name: 'ipone',
            price: 9000
        }
    })
}).then(resp => resp.json())
    .then(console.log)
    .catch(console.error);

5.2.2 也可以自定义验证器

最简单的,组合已有的验证器:

@Min(100)
@Max(10000)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
public @interface Price {
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

也可以自己实现相同逻辑:

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy =  PriceRangeValidator.class )
public @interface PriceRange {
    String message() default "价格不合理";
    float min() default 0;
    float max() default 10000;

    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

class PriceRangeValidator implements ConstraintValidator<PriceRange, Float> {
    private float min, max;

    @Override
    public void initialize(PriceRange constraintAnnotation) {
        min = constraintAnnotation.min();
        max = constraintAnnotation.max();
    }

    @Override
    public boolean isValid(Float price, ConstraintValidatorContext context) {
        // 记载数据库,外部 xml
        return price >= min && price <= max;
    }
}

@CellPhone:

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CellPhoneValidator.class)
public @interface CellPhone {
    String message() default "不是合法的手机编号,应该是11位";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

class CellPhoneValidator implements ConstraintValidator<CellPhone, String> {
    @Override
    public void initialize(CellPhone constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && Pattern.matches("^[0-9]{11}$", value);
    }
}

@OrderStatus:

Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@NotEmpty
@Constraint(validatedBy =  OrderStatusValidator.class )
public @interface OrderStatus {
    String message() default "不是合法的状态,只能是 created/paid/shipped/closed 中的一个";

    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

class OrderStatusValidator implements ConstraintValidator<OrderStatus, String> {
    @Override
    public void initialize(OrderStatus constraintAnnotation) {
    }

    @Override
    public boolean isValid(String status, ConstraintValidatorContext context) {
        return Arrays.asList("created", "paid22", "shipped", "closed").contains(status);
    }
}

5.3 Spring Validator 接口

这是 Spring 验证的标准接口。使用分 3 步:

  1. 定义实现类
  2. 注册(配置到 Controller 或通过配置文件配置到全局)
  3. 配合 @Validated 注解使用

基本过程:




6 Exception (异常处理)

6.1 使用默认的 DefaultHandlerExceptionResolver 异常处理类

6.2 编程式异常处理

在代码中,使用 try…catch 的方式,将所有(相关的)异常,全都处理妥当。

public String add(Emp emp, Model model) {
    try {
        empService.addEmp(emp);
    } catch (DbException e) {
        model.addAttribute("msg", e);
        return "warn";
    } catch (DataException e) {
        model.addAttribute("msg", e.getMessage());
        return "input";
    } catch (MyBatisSystemException e) {
        return "unknown err";
    } catch (Exception e) {
        return "unknown err";
    }

    return "success";
}

6.3 自定义 HandlerExceptionResolver,用于全局异常处理

首先,定义一个 MyExceptionResolver 异常处理类:

public class MyExceptionResolver extends AbstractHandlerExceptionResolver {
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex instanceof DbException) {
            return new ModelAndView("shujukucuowu");
        } else if (ex instanceof TypeMismatchException) {
            return new ModelAndView("shujukucuowu");
        }
        return null;
    }
}

然后,将其在 xml 中注册:

<bean id="exceptionResolver" class="com.nf147.test01.exceptionhandler.MyExceptionResolver" />

然后,容器启动的时候,就不会将 DefaultHandlerExceptionResolver 的实例放到容器里了。

于是,容器里就只有这一个 HandlerExceptionResolver 处理类了。

6.4 使用注解的方式 @ExceptionHandler/@ControllerAdvice

使用 @ExceptionHandler 进行类内异常处理:

@Controller
@RequestMapping("/eee")
public class Emp3Controller {

    @Autowired
    private EmpService empService;

    @RequestMapping(method = RequestMethod.POST, produces = "text/plain; charset=UTF-8")
    public String add(Emp emp, Model model) throws Exception {
        empService.addEmp(emp);
        return "success";
    }

    @RequestMapping(method = RequestMethod.GET)
    public String selectAll(Model model) {
        List<Emp> empList = empService.selectAll();
        model.addAttribute("empList", empList);
        return "emp_index";
    }



    // 下面这些注解,只能作用于本类之中

    @ExceptionHandler(DbException.class)
    public String handlerDbException (Exception ex, Model model, WebRequest request) {
        model.addAttribute("err", ex.getMessage());
        return "err1"; // 默认返回 status 200
    }

    @ExceptionHandler(DataException.class)
    @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "数据库的错误")
    public ModelAndView handlerDbException2 () {
        return new ModelAndView("err2");
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity handlerDbException4 () {
        return ResponseEntity.status(333).body("dkfjs");
    }
}

配合 @ControllerAdvice 使用:

@Component
@ControllerAdvice
public class AllControllerAdvice {
    // 这个处理,对所有的 Controller 都会有效
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.URI_TOO_LONG)
    @ResponseBody
    public String handlerDbException3 () {
        return "ksjfksd";
    }

    @ExceptionHandler(RuntimeException.class)
    public String handlerDbException4 () {
        return "runtime";
    }
}

7 View (视图渲染,服务端渲染)

7.1 ViewResolver

可以同时配置多个视图解析器。

Spring MVC 内部,支持内置的 JSP 解析,还支持经典的 FreeMaker。

另外一个比较推荐使用的模板引擎是 Thymeleaf。

7.2 Spring JSP Taglib

通过这些标签,可以节省非常多的逻辑。比如,在 form 表单上,他们提供了:

  • 回显输入
  • 渲染错误信息

相关的标签有很多,比如:

  • form:form
  • form:input
  • form:error
  • spring:message
  • form:select…

7.3 Thymeleaf

优点:

  • 语法简单
  • 便与调试
  • 其他我不说了

比如 Thymeleaf 的语法如下:

<table>
  <thead>
    <tr>
      <th th:text="#{msgs.headers.name}">Name</th>
      <th th:text="#{msgs.headers.price}">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="prod: ${allProducts}">
      <td th:text="${prod.name}">Oranges</td>
      <td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
    </tr>
  </tbody>
</table>

8 I18N (国际化)

Internationalization 的缩写。

Spring 提供了两个接口用于国际化文件处理:

  1. MessageSource,用于加载资源文件
  2. MessageResolver,用于解析用户所处的位置(Locale)

如果想使用资源文件,只需要在容器里注册一个 MessageSource 的实例即可。 一般情况下,使用 Spring 内置的 ReloadableResourceBundleMessageSource 实现:

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
  <property name="basename" value="classpath:messages" />
  <property name="defaultEncoding" value="GBK" />
</bean>

然后在 classpath 下面创建 messages[_zh].properties 文件:

xxx=23232323
yyy.xxx=ksdjfk
err.aaa=ksdfjjjjj

接下来就可以使用了:

  • 在 JSP 中: <spring:message code="xxx" />
  • 在验证器中: errors.reject("err.aaa");

如果想在 JSR303 验证中使用 message 文件加载错误信息,那么就需要额外配置下内置验证器了:

<mvc:annotation-driven validator="myValidator" />

<bean id="myValidator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
  <property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
  <property name="validationMessageSource" ref="messageSource" />
</bean>

然后就可以了:

@Range(min = 10, max = 100, message = {err.xxx})
Float price;

默认情况下,Spring 使用 AcceptHeaderLocaleResolver 来解析用户的区域。 它本质是调用 request.getLocale() 方法,通过 Accept-Language 来获取的区域。

如果,这种自动确定区域的方式不适合你,那么你需要亲自注册一个 LocaleResolver 来制定 Locale 策略。 可选的策略有 SessionLocaleResolver/CookieLocaleResolver/FixedLocaleResolver:

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
  <property name="defaultLocale" value="zh"/>
</bean>

就这样就可以了。下面是一键切换语言的示例:

@Autowired
private LocaleResolver localeResolver;

@GetMapping("/change-locale/{loc}")
@ResponseBody
public String changeLocale (@PathVariable("loc") String localeStr, HttpServletRequest req, HttpServletResponse resp) {
    Locale locale = new Locale(localeStr);
    localeResolver.setLocale(req, resp, locale);
    return "success";
}

// http://localhost:8080/change-locale/zh

9 Theme (主题)

主题指得是一系列的 css 以及影响页面显示效果的图片等资源的集合。

Spring 中可以根据不同情况加载不同主题。它的实现方式跟 i18n 绝对雷同

两个主要接口:

  • ThemeResolver 用于确定要使用的主题的名字(theme name)
  • ThemeSource 用于加载主题文件(通过 theme name)

所以,要启用主题,只需要注入一个 ThemeSource 的实现,比如,内置的 ResouceBundleThemeSource:

<bean id="themeSource" class="org.springframework.ui.context.support.ResourceBundleThemeSource">
  <property name="basenamePrefix" value="themes." /> <!-- 哪个包下,默认是 classpath 根目录,注意,是包名的写法->
</bean>

<!-- 【可选】 -->
<!-- 默认情况下,使用的是 FixedThemeResolver 来确定主题名字,默认名字为 theme -->
<!-- 可以根据实际情况配置为 SessionThemeResovler/CookieThemeResolver -->
<bean id="themeResolver" class="org.springframework.web.servlet.theme.SessionThemeResolver">
  <property name="defaultThemeName" value="girl" /> <!-- 默认主题文件的名字是 "girl",如果不设置,名为 'theme' -->
</bean>

然后,在 classpath 下面的 themes 文件夹下添加 theme.properties 或 girl.properties 文件:

style=/css/pink.css
body.color=red
footer.bg.image=/image/background.png
global.font.family=宋体

随后,就可以在 jsp 文件中使用上面配置的这些资源信息了:

<html>
  <head>
    <link rel="stylesheet" href="<spring:theme code="style" />">
  </head>

  <body style="color: <spring:theme code="body.color" />">
    正文内容
  </body>
</html>

所以如果使用不同的主题文件,页面上的 css 元素就不会相同,就能得到不同渲染效果的页面。

就这么简单。

如果想在页面上一键切换主题,那么跟上面一键切换语言是同样的逻辑。略。

10 File Upload/Download (文件上传、下载)

10.1 Upload

SpringMVC 中,文件的上传是通过 MultipartResolver 实现的,所以要实现上传,只要注册相应的 MultipartResolver 即可。

MultipartResolver 的实现类有两个:

  1. CommonsMultipartResolver (需要 Apache 的 commons-fileupload 支持,它能在比较旧的 servlet 版本中使用,兼容性好)
  2. StandardServletMultipartResolver (不需要第三方 jar 包支持,它使用 servlet 内置的上传功能,但是只能在 Servlet 3 以上的版本使用)

以 StandardServletMultipartResolver 为例,使用步骤如下。

首先,在 web.xml 中为 DispatcherServlet 配置 Multipart:

<servlet>
  <servlet-name>mvc</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

  <multipart-config>
    <max-file-size>5242880</max-file-size>        <!-- 上传文件的大小限制,比如下面表示 5 M -->
    <max-request-size>10485760</max-request-size> <!-- 一次表单提交中文件的大小限制,必须下面代表 10 M -->
    <file-size-threshold>0</file-size-threshold>  <!-- 多大的文件会被自动保存到硬盘上。0 代表所有 -->
  </multipart-config>
</servlet>

其次,在 spring 中注册 MultipartResolver:

<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

然后,就可以使用了,比如,表单:

<form:form action="/file/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="mfile" />
  <input name="desc" placeholder="文件描述" />
  <input type="submit" value="上传" />
</form:form>

提交后,就可以在 Controller 里这样处理了:

@PostMapping("/upload")
public String upload(MultipartFile mfile) throws Exception {
    String savePath = "xxx";
    if(!mfile.isEmpty()) {
        mfile.transferTo(new File(savePath + mfile.getOriginalFilename()));
    }
    return "file/index";
}

另外可以用 @RequestPart/@RequestParam 注解 MultipartFile 参数。

10.2 Download

使用 ResponseEntity 可以让下载变得简单:

@GetMapping(value = "/download")
ResponseEntity<InputStreamResource> downloadFile() throws IOException {
    FileSystemResource file = new FileSystemResource("/home/vip/logo.png");

    HttpHeaders headers = new HttpHeaders();
    headers.setCacheControl("no-cache, no-store, must-revalidate");
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    headers.setContentLength(file.contentLength());
    headers.setContentDispositionFormData("attachment", file.getFilename());

    return ResponseEntity.ok().headers(headers).body(new InputStreamResource(file.getInputStream()));
}

0.1 代码规范 0.1 异常处理 0.1 没有进行有效过滤 0.1 对于过去学习内容的遗忘 0.1 审题不仔细

10.3 示例

@PostMapping("/book")
public String imageshangchuan(MultipartFile ufile, Model model, HttpServletRequest request, @Valid Book book, BindingResult bookResult) {
    if (bookResult.hasErrors()) {
        return "book_input";
    }

    List<String> errors = new ArrayList<>();
    if (ufile.isEmpty()) {
        errors.add("文件为空错误");
    }
    if (ufile.getSize() > 1024 * 1024 * 5) {
        errors.add("文件超出大小,请重新选择!");
    }
    if (!ufile.getContentType().contains("image/")) {
        errors.add("只允许上传图片文件!");
    }
    if (!errors.isEmpty()) {
        model.addAttribute("errs", errors);
        return "book_input";
    }

    String basePath = request.getServletContext().getRealPath("/img");
    String relativePath; // 图片的保存路径
    try {
        relativePath = makeRelativePath(ufile.getOriginalFilename());
        File target = new File(basePath + relativePath);
        target.getParentFile().mkdir();
        ufile.transferTo(target);
    } catch (IOException e) {
        model.addAttribute("err", "文件上传失败,请重试!");
        return "book_input";
    }

    book.setCover(relativePath);
    try {
        System.out.println("对" + book + "进行保存等操作");
    } catch (Exception e) {
        // 实际业务中,要考虑异常的处理
        model.addAttribute("err", "something");
        return "book_input";
    }
    return "book_home";
}

public String makeRelativePath(String fileName) {
    Date now = new Date();
    String[] fileNames = splitFileString(fileName);
    return String.format("%s%s%supload_%s_%s.%s",
                         File.separator,
                         new SimpleDateFormat("yyyyMMdd").format(now),
                         File.separator,
                         fileNames[0],
                         new SimpleDateFormat("hhmmss").format(now),
                         fileNames[1]
                         );
}

public String[] splitFileString(String fileName) {
    int dotPos = fileName.lastIndexOf(".");
    String ext = fileName.substring(dotPos + 1);
    String name = fileName.substring(0, dotPos);
    return new String[]{name, ext};
}

11 CORS (Cross Origin Resources Share) 跨域

Spring 提供了三种方式:

  1. CorsFilter 过滤器
  2. <mvc:cors> Bean
  3. @CrossOrigin 注解

这三种方式,本质上都是用来配置 CorsConfiguration。

11.1 CorsFilter

首先,依赖 CorsFilter 创建自己的过滤器:

public class MyCorsFilter extends CorsFilter {
    public MyCorsFilter() {
        super(configurationSource());
    }

    private static UrlBasedCorsConfigurationSource configurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Collections.singletonList("http://domain.com"));
        config.setAllowCredentials(true);

        CorsConfiguration config2 = new CorsConfiguration();
        config2.setAllowedOrigins(Collections.singletonList("http://domain.com"));
        config2.setAllowCredentials(true);

        source.registerCorsConfiguration("/**", config);
        source.registerCorsConfiguration("/xxx", config2);

        return source;
    }
}

然后,将其注册为一个过滤器即可。

11.2 <mvc:cors>

<mvc:cors>
  <mvc:mapping path="/xxx"
               allowed-origins="http://localhost:7070"
               allowed-methods="GET, POST"
               allowed-headers="Accept-Charset, Accept, Content-Type"
               allow-credentials="true" />
  <mvc:mapping path="/yyy/*"
               allowed-origins="*"
               allowed-methods="*"
               allowed-headers="*" />
</mvc:cors>

11.3 @CrossOrigin

// 将跨域设置在类上,那么所有的 mapping 都会使用这个策略
// 如果不加参数,那么将会使用配置中的默认参数
@CrossOrigin
public class CORSController {
    public String cors(@RequestParam(defaultValue = "callback") String callback, HttpServletResponse response) {
        // 最原始的方式,手动写请求头
        response.setHeader("Access-Control-Allow-Origin", "http://192.168.163.1:8081");
        return callback + "('hello')";
    }


    // 将跨域设置在方法上
    @RequestMapping("/cors")
    @CrossOrigin(origins = {"http://localhost:8080", "http://remotehost:82323"},
                 methods = {RequestMethod.GET, RequestMethod.POST},
                 allowedHeaders = {"Content-Type", "skfjksdjfk"},
                 allowCredentials = "true",
                 maxAge = 1898978
                 )
    @RequestMapping("/rrr")
    public String rrr(@RequestParam(defaultValue = "callback") String callback) {
        return callback + "('rrr')";
    }
}

12 RESTful/前后端分离

12.1 编码问题

如何设置,才能避免乱码?

Producer/Consumer (生产/消费模式)。

// 请求内容类型必须为 text/html
@RequestMapping(value="/aaa", consumes="text/html");

// 客户端接收 json 且编码为 utf-8
@RequestMapping(value="/bbb", produces="application/json; charset=UTF-8");

全局设置的方式:

<!--启用 mvc 的常用注解-->
<mvc:annotation-driven validator="myValidator" conversion-service="conversionService">
    <mvc:message-converters>
        <!--@ResponseBody 的 UTF-8 编码-->
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <constructor-arg value="UTF-8"/>
        </bean>

        <!-- Fail On Empty Beans... -->
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper">
                <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
                    <property name="failOnEmptyBeans" value="false"/>
                </bean>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

@JsonFormat/@JsonIgnore/…

12.2 跨域问题

@Cors

12.3 结果封装

12.4 统一异常处理

13 Task TODO

13.1 DONE task

  • 要学会从产品的角度考虑问题
  • 要学会站在用户体验的角度进行设计
  • 要根据自己的兴趣和擅长,进行针对性思考学习
  • 麻雀虽小,五脏俱全,管中窥豹,可见一斑

13.2 DONE sub-task[2/2]

13.2.1 DONE 任务需要分类

13.2.2 DONE 任务需要制定优先级

13.3 TODO 任务示例[1/5]

  • [X] 需求分析
  • [ ] 软件详细设计
  • [ ] 编码实现
  • [ ] 测试
  • [ ] 部署发布

Author: unname

Created: 2019-03-22 周五 01:32

Go ahead, never stop.