XhstormR


On a dark desert highway Cool wind in my hair


Spring MVC

Updated on 2017-06-22

MappingJackson2HttpMessageConverter

https://github.com/thymeleaf/thymeleaf

http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

Concept

  • REST:将资源的 状态适合 客户端的 格式 从服务端 传递 至客户端(或者反过来)。
    • 资源通过 URL 进行标识,通过 HTTP Method 进行管理,通过 HTTP Header 表述格式。
      • 资源:URL(统一资源定位器)
      • 格式:Accept、Content-Type 头部信息。
      • 动词:CRUD(增删改查)
        • 幂等:同一操作 执行 任意次数 所产生的影响或结果 相同

Configuration

build.gradle.kts

plugin("war")

compile("org.springframework:spring-webmvc:+")
compile("javax.servlet:javax.servlet-api:+")

compile("org.thymeleaf:thymeleaf-spring4:+")
compile("org.slf4j:slf4j-jdk14:+")

compile("org.hibernate:hibernate-validator:5.+")

compile("com.fasterxml.jackson.core:jackson-databind:+")

Code

AppConfig

import org.springframework.web.filter.CharacterEncodingFilter
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer
import javax.servlet.Filter
import javax.servlet.MultipartConfigElement
import javax.servlet.ServletRegistration

class AppConfig : AbstractAnnotationConfigDispatcherServletInitializer() {
    override fun getServletMappings(): Array<String> = arrayOf("/")

    override fun getRootConfigClasses(): Array<Class<*>> = arrayOf(RootConfig::class.java)

    override fun getServletConfigClasses(): Array<Class<*>> = arrayOf(WebConfig::class.java)

    override fun getServletFilters(): Array<Filter> = arrayOf(CharacterEncodingFilter("UTF-8", true))     添加过滤器

    override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
        val mb = 1024 * 1024L     限制文件最大为 2 MB,请求最大为 4 MB
        registration.setMultipartConfig(MultipartConfigElement("D:/TEMP", 2 * mb, 4 * mb, 0))
    }
}

WebConfig

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.format.FormatterRegistry
import org.springframework.web.multipart.MultipartResolver
import org.springframework.web.multipart.support.StandardServletMultipartResolver
import org.springframework.web.servlet.ViewResolver
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
import org.thymeleaf.spring4.SpringTemplateEngine
import org.thymeleaf.spring4.view.ThymeleafViewResolver
import org.thymeleaf.templateresolver.ITemplateResolver
import org.thymeleaf.templateresolver.ServletContextTemplateResolver
import javax.servlet.ServletContext

@Configuration
@EnableWebMvc     启用 Spring MVC
@ComponentScan("controller")
open class WebConfig : WebMvcConfigurerAdapter() {
    @Bean     1
    open fun templateResolver(servletContext: ServletContext): ITemplateResolver =
            ServletContextTemplateResolver(servletContext).apply {
                this.characterEncoding = "UTF-8"
                this.prefix = "/WEB-INF/views/"
                this.suffix = ".html"
            }

    @Bean     2
    open fun templateEngine(templateResolver: ITemplateResolver): SpringTemplateEngine =
            SpringTemplateEngine().apply { this.setTemplateResolver(templateResolver) }

    @Bean     3
    open fun viewResolver(templateEngine: SpringTemplateEngine): ViewResolver =
            ThymeleafViewResolver().apply {
                this.characterEncoding = "UTF-8"
                this.templateEngine = templateEngine
            }

    @Bean
    open fun multipartResolver(): MultipartResolver = StandardServletMultipartResolver()     提供文件上传支持

    override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) = configurer.enable()     提供静态资源访问(启用 DefaultServletHttpRequestHandler)

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(MyInterceptor()).addPathPatterns("/**")     注册拦截器
    }

    override fun addFormatters(registry: FormatterRegistry) {
        registry.addFormatter(MyDateFormatter())             注册 Formatter
        registry.addConverter(MyStringToDateConverter())     注册 Converter
    }
}

RootConfig

import org.springframework.context.annotation.Configuration

@Configuration
open class RootConfig

MyInterceptor

import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter
import java.lang.Exception
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class MyInterceptor : HandlerInterceptorAdapter() {     拦截器
    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        println("预处理")
        return true     是否放行请求(true 放行,false 拦截)
    }

    override fun postHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any, modelAndView: ModelAndView?) {
        println("后处理")
        if (modelAndView != null) {
            modelAndView.model["msg"] = "信息"
        }
    }

    override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) {
        println("请求结束")
    }
}

基于切面思想的应用:
过滤器:基于 Servlet 容器,能够过滤所有请求,使用回调函数。(Filter)
拦截器:基于 Spring  容器,只能拦截部分请求,使用反射机制。(Interceptor)

MyDateFormatter

import org.springframework.format.Formatter
import java.text.SimpleDateFormat
import java.util.*

class MyDateFormatter : Formatter<Date> {     Formatter:String <-> T
    private val sdf = SimpleDateFormat("yyyy-MM-dd")

    override fun parse(text: String, locale: Locale): Date = sdf.parse(text)

    override fun print(`object`: Date, locale: Locale): String = sdf.format(`object`)
}

MyStringToDateConverter

import org.springframework.core.convert.converter.Converter
import java.text.SimpleDateFormat
import java.util.*

class MyStringToDateConverter : Converter<String, Date> {     Converter:S --> T
    private val sdf = SimpleDateFormat("yyyy-MM-dd")

    override fun convert(source: String): Date = sdf.parse(source)
}

entity

Account

package entity

import javax.validation.constraints.Min
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size

data class Account(
        @field:NotNull     校验属性
        @field:Size(min = 2, max = 4)
        var username: String? = null,
        @field:NotNull
        @field:Size(min = 6, max = 12)
        var password: String? = null,
        @field:NotNull
        @field:Min(18)
        var age: Int? = null
)

controller

HomeController

package controller

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod

@Controller     声明为控制器
@RequestMapping("/")     映射至 127.0.0.1:8080/
class HomeController {
    @RequestMapping(method = arrayOf(RequestMethod.GET))     处理 GET 请求
    fun home(): String {
        return "home"     解析为 /WEB-INF/views/home.html 视图
    }
}

AController

package controller

import entity.Account
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.validation.Errors
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.servlet.mvc.support.RedirectAttributes
import java.io.File
import javax.validation.Valid

@Controller
@RequestMapping("/a")
class AController {     视图
    查询参数:127.0.0.1:8080/a?username=张三&password=123456&age=20
    @RequestMapping(method = arrayOf(RequestMethod.GET))     可简化为 @GetMapping
    fun a(@RequestParam(defaultValue = "张三") username: String,
          @RequestParam(defaultValue = "123456") password: String,
          @RequestParam(defaultValue = "20") age: Int,
          model: Model): String {
        if (!model.containsAttribute("account")) {     跨重定向请求传递复杂数据
            model.addAttribute("account", Account(username, password, age))
            可简化为
            model.addAttribute(Account(username, password, age))     根据对象类型推断 Key
        }
        return "a"
    }

    进一步简化:根据请求路径推断视图(a),根据对象类型推断 Key(account)
    @RequestMapping(method = arrayOf(RequestMethod.GET))
    fun a(@RequestParam(defaultValue = "张三") username: String,
          @RequestParam(defaultValue = "123456") password: String,
          @RequestParam(defaultValue = "20") age: Int): Account {
        return Account(username, password, age)
    }

    路径变量:127.0.0.1:8080/a/张三/123456/20
    @RequestMapping(path = arrayOf("/{username}/{password}/{age}"), method = arrayOf(RequestMethod.GET))
    fun b(@PathVariable username: String,
          @PathVariable password: String,
          @PathVariable age: Int,
          model: Model): String {
        if (!model.containsAttribute("account")) {
            model.addAttribute(Account(username, password, age))
        }
        return "a"
    }

    表单参数
    @RequestMapping(path = arrayOf("/form"), method = arrayOf(RequestMethod.POST))
    fun c(@Valid account: Account, errors: Errors, model: Model): String {     校验属性
        if (errors.hasErrors()) {     检查校验是否出错
            return "redirect:/"     重定向(redirect:),请求转发(forward:)
        }
        model.addAttribute("username", account.username)     跨重定向请求传递简单数据
        model.addAttribute("password", account.password)
        model.addAttribute("age", account.age)

        return "redirect:/a/{username}/{password}/{age}"     通过路径变量的形式传递数据:127.0.0.1:8080/a/张三/123456/20
        或者
        return "redirect:/a"     通过查询参数的形式传递数据:127.0.0.1:8080/a?username=张三&password=123456&age=20
    }

    跨重定向请求传递复杂数据:
    @RequestMapping(path = arrayOf("/form"), method = arrayOf(RequestMethod.POST))
    fun c(@Valid account: Account, errors: Errors, model: RedirectAttributes): String {
        if (errors.hasErrors()) {
            return "redirect:/"
        }
        model.addFlashAttribute(account)     跨重定向请求传递复杂数据
        return "redirect:/a"     通过 flash 属性传递复杂数据
    }

    文件上传
    @RequestMapping(path = arrayOf("/upload"), method = arrayOf(RequestMethod.POST))     可简化为 @PostMapping
    fun d(@RequestPart myPic: MultipartFile): String {
        myPic.transferTo(File("D:/TEMP2/${myPic.originalFilename}"))
        return "redirect:/"
    }

    异常处理(作用于单个控制器)
    @ExceptionHandler(Throwable::class)
    fun handleError(): String {
        return "error"
    }
}

BController

package controller

import entity.Account
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.validation.Errors
import org.springframework.web.bind.annotation.*
import javax.validation.Valid

@RestController     转换响应体(类级别)(ResponseBody + Controller)
@RequestMapping("/b")
class BController {     REST API
    //@ResponseBody     转换响应体(方法级别)
    @RequestMapping(path = arrayOf("/jsonOut"), method = arrayOf(RequestMethod.GET), produces = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE))     限制 Accept 头部信息
    @ResponseStatus(HttpStatus.OK)     指定 HTTP 状态码
    fun a(): Account {
        return Account("张三", "123456", 20)
    }

    @RequestMapping(path = arrayOf("/jsonInOut"), method = arrayOf(RequestMethod.POST), consumes = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE))     限制 Content-Type 头部信息
    @ResponseStatus(HttpStatus.CREATED)
    fun b(@RequestBody @Valid account: Account, errors: Errors): Account {     转换请求体
        if (errors.hasErrors()) {
            throw AccountNotFoundException(9527)
        }
        return account.apply { this.username = "李四" }
    }

    @ExceptionHandler(AccountNotFoundException::class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    fun handleError(e: AccountNotFoundException): Error {
        return Error(4, "账户 ${e.accountId} 未找到")
    }
}

data class AccountNotFoundException(
        val accountId: Int
) : RuntimeException()

data class Error(
        val code: Int,
        val message: String
)

CController

package controller

import entity.Account
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
@RequestMapping("/c")
class CController {
    http://localhost:8080/c/base.do?age=123
    Int :Kotlin 编译为原始类型,参数必选
    Int?:Kotlin 编译为包装类型,参数可选
    @GetMapping("/base.do")
    fun base(age: Int?) = age.toString()

    Array
    http://localhost:8080/c/array.do?names=Tom&names=Lucy&names=Jim
    @GetMapping("/array.do")
    fun array(names: Array<String>) = Arrays.toString(names)

    Date
    http://localhost:8080/c/date.do?date=2017-07-15
    默认时间格式为 DateFormat.SHORT:15/07/17 14:30
    @GetMapping("/date.do")
    fun date(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) date: Date) = date

    Object
    http://localhost:8080/c/object.do
    /*
    {
        "username": "张三",
        "password": "123456",
        "age": 20
    }
    */
    @PostMapping("/object.do")
    fun `object`(@RequestBody account: Account) = account

    List,Set
    http://localhost:8080/c/list.do
    /*
    [
        {
            "username": "张三",
            "password": "123456",
            "age": 20
        },
        {
            "username": "张三",
            "password": "123456",
            "age": 20
        }
    ]
    */
    @PostMapping("/list.do")
    fun list(@RequestBody accounts: List<Account>) = accounts

    Map
    http://localhost:8080/c/map.do
    /*
    {
        "a": {
            "username": "张三",
            "password": "123456",
            "age": 20
        },
        "b": {
            "username": "张三",
            "password": "123456",
            "age": 20
        }
    }
    */
    @PostMapping("/map.do")
    fun map(@RequestBody accounts: Map<String, Account>) = accounts
}

AppErrorHandler

package controller

import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice     控制器通知
class AppErrorHandler {
    @ExceptionHandler(Throwable::class)     异常处理(作用于所有控制器)
    fun handleError(): String {
        return "error"
    }
}

HTML

a.html

<html xmlns:th="http://www.thymeleaf.org">     Thymeleaf 命名空间
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${account}"></p>
</body>
</html>

变量表达式:${ },基于 SpEL 表达式。
选择表达式:*{ },基于选中对象。
路径表达式:@{ }。
对象表达式:#{ }。

b.html

<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/a/form" method="post">
    <input name="username" type="text"><br>
    <input name="password" type="text"><br>
    <input name="age" type="number"><br>
    <input type="submit"><br>
</form>
</body>
</html>

c.html

<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/a/upload" method="post" enctype="multipart/form-data">     以 multipart 格式提交表单
    <input name="myPic" type="file"><br>
    <input type="submit"><br>
</form>
</body>
</html>
TOP