spring boot2下rest风格映射问题解决及源码解析

  • Post author:
  • Post category:其他



目录


1.什么是rest风格


2.初步假设测试


3.源码探究及解决


4.具体配置办法


5.扩展


1.什么是rest风格


HTTP


协议发展的过程中,提出了很


多的规则,但是这些规则有


些烦琐,于是又提出了一种风


格约定,它便是rest


风格


。在


现今流行的微服务中,这样的风格甚


至被推荐为各


个微服


务系统之间用于交互的方式。首先在


REST


风格


中,


每一个资源都只是对应着一个


网址,而这个网址


应该是一个名词 ,不存在动词





这代表对


资源的操作。

核心是三个名词:资源,表现层,状态转换

资源:


它可以是系统权限用户、角色和菜单


,也可以


些媒体类型 如文本、图片、歌曲等


表现层:有了资源还需要确定


如何表现这个


资源。


例如一


个用


户可以用


JSON,


XML


或者其他


的形式表现出来




状态转换:


现实中的资源并不是一成不变的,一个资源可以经历增删改查的过程。
RESTFul API的一些最佳实践原则:
  1. 使用HTTP动词表示增删改查资源, GET:查询,POST:新增,PUT:更新,DELETE:删除
  2. URI使用名词而不是动词,且推荐使用复数
  3. 保证HEAD和GET方法是安全的,不会对资源状态有所改变(污染)。比如:严格杜绝如下情况:GET/deleteProduct?id=1
  4. 资源的地址推荐用嵌套结构。比如:GET/friends/10375923/profile
  5. 警惕返回结果的大小。如果过大,及时进行分页(pagination)或者加入限制(limit)。HTTP协议支持分页(Pagination)操作,在Header中使用Link即可。
  6. 使用正确的HTTP Status Code 表示访问状态。
  7. 返回结果必须使用JSON
  8. HTTP状态码,在REST中都有特定的意义:200,201,202,204,400,401,403,500。比如401表示用户身份认证失败,403表示你验证身份通过了,但这个资源你不能操作。
  9. 如果出现错误,返回一个错误码。

参考:

https://blog.csdn.net/ayqy42602/article/details/107689748

2.初步假设测试

先写个测试控制器

@RestController
public class HelloController {
    @RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getUser(){
        return "get-zs";
    }
    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String saveUser(){
        return "post-zs";
    }
    @RequestMapping(value = "/user",method = RequestMethod.DELETE)
    public String deleteUser(){
        return "delete-zs";
    }
    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    public String putUser(){
        return "put-zs";
    }
}

再写个前端页面测试请求,想当然会对应匹配跳转

<body>
<h1>表单提交</h1>
<!-- 表单提交用户信息,注意字段的设置,直接是*{} -->
    <form action="/user"  method="get">
        <input value="rest get提交" type="submit" />
    </form>
    <form action="/user" method="post">
        <input value="rest post提交" type="submit" />
    </form>
    <form action="/user" method="put">
        <input value="rest put提交" type="submit" />
    </form>
    <form action="/user" method="delete">
        <input value="rest delete提交" type="submit" />
    </form>
</body>

但是很可惜,结果出乎意料,如下

put请求和delete请求没有如所想的跳到对应的put页面和delete页面,按住页面中那个method属性进去可以看见method只有两个属性(分别是get和post),

可见浏览器form表单只支持GET与POST请求,而DELETE、PUT等method并不支持

3.源码探究及解决

spring3.0添加了一个过滤器,可以将这些请求转换为标准的http方法,使得支持GET、POST、PUT与DELETE请求,该过滤器为HiddenHttpMethodFilter。

web请求分配映射这些功能原来都是springmvc管的,现在被spring boot集成后,就要首先研究WebMvcAutoConfiguration这个自动配置类,进入后可发现类似这个功能的过滤器

@ConditionalOnMissingBean,它是修饰bean的一个注解,主要实现的是,当你的bean被注册之后,如果注册相同类型的bean,就不会成功,它会保证你的bean只有一个,即你的实例只有一个,当你注册多个相同的bean时,会出现异常,以此来告诉开发人员。@ConditionalOnMissingBean(HiddenHttpMethodFilter.class) 意思是容器中没有HiddenHttpMethodFilter这个bean时才把这个OrderedHiddenHttpMethodFilter组件纳入容器,如果有的话下面就不会再执行了

@ConditionalOnProperty源码如下

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional({OnPropertyCondition.class})
public @interface ConditionalOnProperty {
    String[] value() default {}; //数组,获取对应property名称的值,与name不可同时使用

    String prefix() default "";//property名称的前缀,可有可无

    String[] name() default {};//数组,property完整名称或部分名称(可与prefix组合使用,组成完整的property名称),与value不可同时使用

    String havingValue() default "";//可与name组合使用,比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置

    boolean matchIfMissing() default false;//缺少该property时是否可以加载。如果为true,没有该property也会正常加载;反之报错

    boolean relaxedNames() default true;//是否可以松散匹配,至今不知道怎么使用的
}

@ConditionalOnProperty(prefix = “spring.mvc.hiddenmethod.filter”, name = “enabled”, matchIfMissing = false)这句话的意思是如果缺失了spring.mvc.hiddenmethod.filter.enabled这个属性,直接不自动装配,如果有这个属性,还得这个属性不为false才行(为空或值为其他什么值都没问题),详情参考:

http://www.voidcn.com/article/p-fcxykflh-brz.html

打开spring.mvc.hiddenmethod.filter.enabled一看就知道它默认是false,即不能使用这个过滤,要自己手动开启

这两个注解合起来的逻辑就是先判断容器里有没有HiddenHttpMethodFilter这个bean,如果我自己已经重写了一个,那下面就不能自动装配了,接下来检查这个spring.mvc.hiddenmethod.filter.enabled属性是否存在,不存在也就不用往下搞了(当然matchIfMissing这个判断默认就是false,加不加无所谓),然后如果这个属性存在还得不为false(其实就是个开关作用,默认false为关闭,也控制下面不能接下去搞,要开启得在配置文件中设置为true,意思是开启这个过滤器)。历尽重重关卡,最后终于可以把OrderedHiddenHttpMethodFilter这个bean纳入容器,让我们进去里面具体看下。

很显然它就是继承了HiddenHttpMethodFilter这个类,那么它做的事情默认没修改的话就是它的父类做的事情,那让我们再进它的父类HiddenHttpMethodFilter一窥究竟。

首先看看它的作用说明

大致的意思是这个类的作用是将post请求转换为其他http的方法,当前的浏览器仅仅支持get和post请求,要是想发送其他请求的方法有个通用的技巧,就是用一个隐藏字段,属性名叫“_method”,赋的值就是真实的发送请求。但是这个表单的发送请求必须得为post,如此才能被拦截器拦截,再通过包装处理变为真正的对应http请求。注意一点:这个被转换的http请求只能是“put”,“delete”,“patch”。还有这个“_method”默认参数名也可以在配置文件内改名为自己取的值。写法如下

下面来根据源代码看看

private static final List<String> ALLOWED_METHODS =Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
					HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

/** Default method parameter: {@code _method}. */
	public static final String DEFAULT_METHOD_PARAM = "_method";

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpServletRequest requestToUse = request;

		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
				String method = paramValue.toUpperCase(Locale.ENGLISH);
				if (ALLOWED_METHODS.contains(method)) {
					requestToUse = new HttpMethodRequestWrapper(request, method);
				}
			}
		}

		filterChain.doFilter(requestToUse, response);
	}

核心的就是这个doFilterInternal方法,post请求才被拦截,是post请求且不为空,就取出这个“_method”属性对应的值,取出后要是不为空就统一转为大写,然后判断一下是不是PUT,DELETE,PATCH这三者之一,是的话就包装后放行。

提示:如果是客户端直接发起的请求,如postman模拟,或者安卓之类的发送put,delete可以直接发送,无需过滤包装放行。springboot之所以默认该过滤器是关闭的是因为现在都提倡前后端分类开发,前端页面不在这写,也就自然不会遇到这个问题,要是页面也放在这写,那么这个过滤器可以自己配下。

4.具体配置办法

上面写的比较零散,这里再来重复以下rest风格请求失败的解决办法

第一步:在application.properties里面开启这个过滤器,加入这句话

spring.mvc.hiddenmethod.filter.enabled=true

第二步:在表单提交时加入“_method”这个属性,对应value为真实请求方式,注意表单提交的方法得是post请求例如下面

 <form action="/user" method="post">
        <input name="_method" type="hidden" value="PUT">
        <input value="rest put提交" type="submit" />
    </form>

第三步:在控制器里面使用对应注解,例如put风格可以直接使用@PutMapping这个注解

@PutMapping("/user")
    public String putUser(){
        return "put-zs";
    }

5.扩展

在ioc容器里面没有HiddenHttpMethodFilter这个bean是进行下面的自动配置的前提,那么我们自然可以不用它的,自己注册一个HiddenHttpMethodFilter组件放在ioc容器里面,灵活性更高。

下面实现一个个性化的小效果来说明这一思想

因为源码里面是提供了修改methodParam属性的接口的

测试可以发现此时,此时属性名已经被改为my_method才能生效了



版权声明:本文为qq_37373250原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。