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 提供了两种方法将资源转换为发送给客户端的表述形式:
Content Negotiation
,选择一个视图,将模型渲染为呈现给客户端的表述形式;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"; }
如果想使用第二种方式,即直接响应内容而非渲染视图,手段有很多:
- 在 handler 上添加
@ResponseBody
注解 - 在 Controller 上添加
@RestController
注解 - 让 handler 直接返回一个
HttpEntity
对象 - 当然,如果在 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 (类型转换)
三种方式:
- PropertyEditor
- Convertor
- 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 验证器,只需要:
- 定义验证注解
- 增加验证器(ConstraintValidator)
- 正常使用
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 步:
- 定义实现类
- 注册(配置到 Controller 或通过配置文件配置到全局)
- 配合 @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 提供了两个接口用于国际化文件处理:
MessageSource
,用于加载资源文件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 的实现类有两个:
CommonsMultipartResolver
(需要 Apache 的 commons-fileupload 支持,它能在比较旧的 servlet 版本中使用,兼容性好)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 提供了三种方式:
CorsFilter
过滤器<mvc:cors>
Bean@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]
需求分析[ ]
软件详细设计[ ]
编码实现[ ]
测试[ ]
部署发布