Spring MVC 探索(二)

DispatcherServlet 请求处理过程

  • 请求处理如下
1、首先查找 WebApplicationContext,当作controller和其它元素在处理过程中可以使用
的属性并与request绑定在一起,它通过默认内置的
DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 当作key来绑定。

2、其中 local resolver 与request 绑定在一起,为了将请求(提交的视图、准备的数据等)
解析为本地的并使用,若不需要,则不需要该解析器。

3、其中 theme resolver 与 request 绑定在一起,为了决定视图view使用哪个theme,若
不需要则可以忽略它。

4、如果你配置了multipart resolver,则会检查多媒体请求,若找到,则会将该请求包装为
MultipartHttpServletRequest 被其它元素进行深层次的处理

5、一个合适的 handler 被找到,则执行链与这个handler相关联(preprocessors, 
postprocessors, and controllers),并按照准备model、提交等顺序执行,对于注解 controllers,
响应可以被提交(在HandlerAdapter中),而不是返回视图view。

6、如果返回model,则会显示view,如果没有model返回(由于前置拦截器或者后置拦截器
由于安全的原因拦截请求),则没有view会被返回,因为可能这个请求已经处理完成了
  • 注意
该 HandlerExceptionResolver beans在 WebApplicationContext 中被声明用来处理在
请求处理过程中抛出的异常,这些异常解析器允许自定义逻辑解决异常

你可以自定义DispatcherServlet 实例通过添加Servlet 初始化参数(init-params元素)
到Servlet 声明,支持的参数:contextClass、contextConfigLocation、namespace
throwExceptionIfNoHandlerFound
  • HandlerExceptionResolver 异常处理
若在请求处理过程中,异常处理器无法处理该异常,则可以向servlet容器抛出,并设置
响应状态码(4xx,5xx),servlet 容器将提交到默认的错误页面,为了定制错误页面,你
可以在容器中定制,在web.xml中声明错误的页面,当异常冒泡,或响应包含错误码时,
servlet容器会dispatch到错误的请求页面(eg:/error),该请求将会继续被 DispatcherServlet处理,
则我们可以匹配一个controller来处理异常信息和返回视图添加返回响应信息

@RestController
public class ErrorController {

    @RequestMapping(path = "/error")
    public Map<String, Object> handle(HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));
        return map;
    }
}

Servlet API 没有提供Java创建错误页面的方式,但我们可以通过WebApplicationInitializer 和
最小化的web.xml来实现

Annotated Controllers

  • 简介
Spring MVC 提供了基于注解编程的模式,@Controller 和 @RestController 组件来
表明请求映射,请求输入、异常处理等,有着灵活的方法签名。
如:
@Controller
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}
  • 声明
你可以在 Servlet's 的 WebApplicationContext 通过使用 Spring的 标准 bean 定义来
定义 controller beans。@Controller 模式允许自动检测与 Spring 通常支持的 @Component
一样,支持自动扫描在 classpath 下的 bean 并自动注册它们,它也是注解类的一种模式,
表明是web组件的角色
  • Java配置
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

    // ...
}
  • Xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

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

    <!-- ... -->

</beans>
  • @RestController
该注解是一个组合注解,是 @Controller and @ResponseBody 的组合注解,即
可以直接写响应内容和视图名称
  • Request Mapping
@RequestMapping 将请求和 controller 方法进行映射,它有各种属性与URL匹配,
HTTP method,request parameters,and media types,该注解可以在类级别来表明
共享的映射,也可以在方法上使用来定位具体的方法,也有一些简写的注解:
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping

如:
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
  • URL patterns
可以使用下列总体模式和通配符
. ? 匹配一个字符
. * 匹配 path 片段中 0 个或更多的 path 片段
. ** 匹配0个或多个 path 片段

可以通过 @PathVariable 来访问URI中变量的值
eg:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}

URI变量会自动转换为对应的类型或抛出 TypeMismatchException,默认支持基本的数据类型
你也可以注册支持其它的数据类型 Type Conversion 和 DataBinder.

也可以使用占位符${…​},在启动的时候通过PropertyPlaceHolderConfigurer 来解析local,
system,environment和其它属性资源

Spring MVC 使用 PathMatcher 和 AntPathMatcher 实现来匹配URI路径
  • pattern comparison模式比较
当多个模式匹配URL时,必须比较找到最匹配的,该工作通过
AntPathMatcher.getPatternComparator(String path) 来完成,查找哪个模式更具体。

1、一个 pattern 如果有较少的URI变量(数量为1),单个通配符(数量为1),两个通配符
(数量为2),则该 pattern 为比较模糊的,给定一个分数,则较长的模式将会被匹配,若
分数和长度相等,则包含更多URI变量的会比包含通配符的优先匹配

2、默认匹配模式为(/**)在得分中会被排除,并在排序中总是会排在最后顺序,同时,前缀
pattern(/public/**)也会被认为比其它不包含两个通配符的pattern 更加模糊

3、更多匹配比较细节,可查看AntPatternComparator in AntPathMatcher类
  • URL匹配比较方法
/**
    * Compare two patterns to determine which should match first, i.e. which
    * is the most specific regarding the current path.
    * @return a negative integer, zero, or a positive integer as pattern1 is
    * more specific, equally specific, or less specific than pattern2.
    */
@Override
public int compare(String pattern1, String pattern2) {
    PatternInfo info1 = new PatternInfo(pattern1);
    PatternInfo info2 = new PatternInfo(pattern2);

    if (info1.isLeastSpecific() && info2.isLeastSpecific()) {
        return 0;
    }
    else if (info1.isLeastSpecific()) {
        return 1;
    }
    else if (info2.isLeastSpecific()) {
        return -1;
    }

    boolean pattern1EqualsPath = pattern1.equals(path);
    boolean pattern2EqualsPath = pattern2.equals(path);
    if (pattern1EqualsPath && pattern2EqualsPath) {
        return 0;
    }
    else if (pattern1EqualsPath) {
        return -1;
    }
    else if (pattern2EqualsPath) {
        return 1;
    }

    if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) {
        return 1;
    }
    else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) {
        return -1;
    }

    if (info1.getTotalCount() != info2.getTotalCount()) {
        return info1.getTotalCount() - info2.getTotalCount();
    }

    if (info1.getLength() != info2.getLength()) {
        return info2.getLength() - info1.getLength();
    }

    if (info1.getSingleWildcards() < info2.getSingleWildcards()) {
        return -1;
    }
    else if (info2.getSingleWildcards() < info1.getSingleWildcards()) {
        return 1;
    }

    if (info1.getUriVars() < info2.getUriVars()) {
        return -1;
    }
    else if (info2.getUriVars() < info1.getUriVars()) {
        return 1;
    }

    return 0;
}
  • Suffix Match 后缀匹配
Spring MVC 默认执行.* 后缀匹配,因此,如一个controller 映射到 /person,则会隐式的
匹配 /person.* ,然后使用文件扩展名来解释请求内容来替换Accept header,
如:/person.pdf,/person.xml等

当浏览器使用Accept header 发送请求时使用文件扩展名是必须的,现在,使用文件扩展名
已经是非必须的,使用Accept header是个更好的选择。

过去使用文件扩展名会在各种方式上有很多问题,当在URI变量、path 参数和URI 编码时使
用会导致各种歧义,对于URL的安全和授权校验也变得困难

为了彻底的关闭文件扩展名,你可以设置如下:

.useSuffixPatternMatching(false), see PathMatchConfigurer

.favorPathExtension(false), see ContentNegotiationConfigurer
  • 指定请求的 Consumable Media Types consumes
可以指定Content-Type来限定映射请求

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

consumes 支持!表达式,如!text/plain则表示除了 text/plain的内容类型
都可以,该属性也可以在类上使用,不过方法上的该属性会覆盖类上的属性
  • 指定响应的 Producible Media Types produces
可以定位到基于Accept header请求的映射,列出方法返回的媒体类型,如:

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

该媒体类型也可以指定字符集编码,也支持!表达式,对于JSON内容类型,应该指定
具体的编码字符集
  • HTTP HEAD
@GetMapping( @RequestMapping(method=HttpMethod.GET))支持 HTTP HEAD 请求映射,
Controller 方法不需要做任何改变,一个响应包装类在 javax.servlet.http.HttpServlet 应用,
确保 Content-Length header 被设置为字节数(实际没有写任何内容到响应结果)

protected void doHead(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    NoBodyResponse response = new NoBodyResponse(resp);
    
    doGet(req, response);
    response.setContentLength();
}

 HTTP HEAD 隐式的被 HTTP GET 支持,进行 HTTP HEAD 请求时实际调用的为doGet()
  • HTTP OPTIONS
默认 HTTP OPTIONS 被处理成设置响应的 Allow 为@RequestMapping指定的请求方法,
若不指定则默认设置为匹配所有的方法,所以,controller 中的方法应该总是指定请
求方法类型

protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    Method[] methods = getAllDeclaredMethods(this.getClass());
    
    boolean ALLOW_GET = false;
    boolean ALLOW_HEAD = false;
    boolean ALLOW_POST = false;
    boolean ALLOW_PUT = false;
    boolean ALLOW_DELETE = false;
    boolean ALLOW_TRACE = true;
    boolean ALLOW_OPTIONS = true;
    
    for (int i=0; i<methods.length; i++) {
        String methodName = methods[i].getName();
        
        if (methodName.equals("doGet")) {
            ALLOW_GET = true;
            ALLOW_HEAD = true;
        } else if (methodName.equals("doPost")) {
            ALLOW_POST = true;
        } else if (methodName.equals("doPut")) {
            ALLOW_PUT = true;
        } else if (methodName.equals("doDelete")) {
            ALLOW_DELETE = true;
        }
        
    }
    
    // we know "allow" is not null as ALLOW_OPTIONS = true
    // when this method is invoked
    StringBuilder allow = new StringBuilder();
    if (ALLOW_GET) {
        allow.append(METHOD_GET);
    }
    if (ALLOW_HEAD) {
        if (allow.length() > 0) {
            allow.append(", ");
        }
        allow.append(METHOD_HEAD);
    }
    if (ALLOW_POST) {
        if (allow.length() > 0) {
            allow.append(", ");
        }
        allow.append(METHOD_POST);
    }
    if (ALLOW_PUT) {
        if (allow.length() > 0) {
            allow.append(", ");
        }
        allow.append(METHOD_PUT);
    }
    if (ALLOW_DELETE) {
        if (allow.length() > 0) {
            allow.append(", ");
        }
        allow.append(METHOD_DELETE);
    }
    if (ALLOW_TRACE) {
        if (allow.length() > 0) {
            allow.append(", ");
        }
        allow.append(METHOD_TRACE);
    }
    if (ALLOW_OPTIONS) {
        if (allow.length() > 0) {
            allow.append(", ");
        }
        allow.append(METHOD_OPTIONS);
    }
    
    resp.setHeader("Allow", allow.toString());
} 
  • 自定义注解
Spring MVC支持组合注解来处理请求映射,如@GetMapping,也支持根据自己的逻辑来处理请求逻辑,你可以
重写 RequestMappingHandlerMapping 中的getCustomMethodCondition  方法来实现自己的处理逻辑
  • 显示注册
你可以编码方式注册handler 方法,如不同URL下同一处理程序的不同实例:

@Configuration
public class MyConfig {

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

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

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

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

}

1、注入目标handler,在处理 controller 的handler
2、准备请求映射的元数据
3、获取要处理的方法
4、添加注册
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值