其他分享
首页 > 其他分享> > http和springboot的一些零碎记录

http和springboot的一些零碎记录

作者:互联网

文章目录

HTTP提交数据的方式总结

常用的方式是get和post,get用于查询,post用于数据的更新,这两种方法的不同,以及springboot mvc解析的不同,postman模拟请求时候配置的不同,总结记录下

基于Http协议,根据Http的请求方法对应的数据传输能力把Http请求分为Url类请求Body类请求,Url类请求包括但不限于GET、HEAD、OPTIONS、TRACE 等请求方法。Body类请求包括但不限于POST、PUT、PUSH、PATCH、DELETE 等请求方法。

因此对于GET请求来说,是不支持数据在http body内的请求方式的。POST方式不仅支持Url类请求也支持body类请求。

GET请求通常用于简单的查询,因为参数会暴露在url上,且受限于url长度限制,复杂条件查询可用POST请求。

GET请求只能用于Content-Type: application/x-www-form-urlencoded,这种请求方式key value作为参数拼接在url后面。

POST请求我们通常使用的Content-Type为application/x-www-form-urlencoded、application/json、

GET请求

请求的参数只能在url上

case1.请求路径在url上

比如http://localhost:8555/order/path/1024,这个1024就是路径参数

对应的Controller代码接收参数如下,使用@PathVariable进行接收参数,

@RequestMapping(value = "/order/path/{id}", method = RequestMethod.GET)
public String order(@PathVariable("id") Integer id) {
    return id+"";
}

使用curl命令如下

curl -X GET --header 'Accept: text/plain' 'http://localhost:8111/order/path/1024'

postman请求如下图,直接输入请求的url即可,其它不需要输入。

image-20201211104211583

通过postman的console查看http报文如下:

GET /order/path/1024 HTTP/1.1
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Cache-Control: no-cache
Postman-Token: 9a70a3bc-dab4-4159-8f93-a6d60493af17
Host: localhost:8111
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Fri, 11 Dec 2020 02:23:51 GMT

1024

发现并没有上送Content-Type,没有则默认使用application/x-www-form-urlencoded

case2.请求参数拼接在url后面

这种请求方式参数通过key value格式追加到url后面

比如http://localhost:8111/order/form?name=zhangsan&age=20

对应Controller代码接收参数如下,使用@RequestParam参数进行参数绑定

@RequestMapping(value = "/order/form", method = RequestMethod.GET)
public String order(@RequestParam(value = "name") String name, @RequestParam(value = "age") Integer age) {
    return name+age;
}

客户端使用curl请求方式

curl -X GET --header 'Accept: text/plain' 'http://localhost:8111/order/form?name=zhangsan&age=20'

postman请求方式,这种请求即把参数通过key value追加到了url末尾,

image-20201211104627540

在query params输入参数,会自动把参数拼接追加到url的末尾,对应的postman console如下

GET /order/form?name=zhangsan&age=20 HTTP/1.1
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Cache-Control: no-cache
Postman-Token: 8497cafc-fa8a-466d-a2e9-62020f458062
Host: localhost:8111
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 10
Date: Fri, 11 Dec 2020 02:46:15 GMT

zhangsan20

发现也并没有上送Content-Type,没有则默认使用application/x-www-form-urlencoded。

http的request header内还有个重要的Content-Length,这个字段表示http body的数据长度,因为用get请求,参数只能在url上,是没有http body的,因此Content-Length为0,因此对于get请求来说,http header是没有Content-Length。

case3.请求参数在http body

case3.1.content-type=multipart/form-data方式

代码和case2一样,使用postman请求,body是form-data

image-20201214201748158

发现竟然请求成功了。postman console如下

GET /order/form HTTP/1.1
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Cache-Control: no-cache
Postman-Token: e949875d-9d9c-432b-846d-5db171f36a60
Host: localhost:8111
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------538007265269603903797417
Content-Length: 269
----------------------------538007265269603903797417
Content-Disposition: form-data; name="name"

wangwu
----------------------------538007265269603903797417
Content-Disposition: form-data; name="age"

22
----------------------------538007265269603903797417--
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 8
Date: Mon, 14 Dec 2020 12:17:12 GMT
wangwu22

发现postman的form-data实际是multipart/form-data,这种请求方式通常是上传附件时候使用,看到这里有Content-Length,使用body的时候才会有该header熟悉。

get方式使用multipart/form-data方式实际很少,可以忽略,知道使用postman有这个方式即可。

case3.2.content-type=x-www-form-urlencode方式

使用x-www-form-urlencode方式,发现请求报错

image-20201214202321532

后台报错如下:Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'name' is not present],参数无法解析,postman http解析如下

image-20201214202459544

为什么content-type是application/x-www-form-urlencoded,也无法解析请求参数呢?因为请求参数是在body,而非url,get请求请求参数只能在url上(multipart/form-data是例外),@RequestParam对于get请求,只会去解析url上的解析参数。

case3.3.content-type=application/json方式

使用json格式进行查询,请求成功,代码如下

@RequestMapping(value = "/order/json", method = RequestMethod.GET)
public String orderJson(@RequestBody  TestParam param) {
    return JSON.toJSONString(param);
}

@Data
public class TestParam {
	private Integer age;
	private String name;
}

@RequestBody对于get请求,会去解析http body。

postman console如下

image-20201214204331158

postman console如下

image-20201214204053164

翻车了,感觉和之前理解的GET请求参数只能放url是不一样的,GET请求也可以用json格式请求,请求数据放http body。

POST请求

case1.请求路径在url上

测试代码如下

@RequestMapping(value = "/order/path/{id}", method = RequestMethod.POST)
public String order(@ApiParam("消息id") @PathVariable("id") Integer id) {
    return id+"";
}

使用curl命令

curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/plain' 'http://localhost:8111/order/path/1024'
#和get请求命令不同,使用post请求使用,默认Content-Type: application/json
#使用content-type是x-www-form-urlencode也是可以
curl -X POST --header 'Content-Type: x-www-form-urlencode' --header 'Accept: text/plain' 'http://localhost:8111/order/path/1024'

postman请求和console结果如下,无content-type,说明使用的是x-www-form-urlencode

image-20201214210403594

case2.请求参数拼接在url后面

测试代码如下

@RequestMapping(value = "/order/form", method = RequestMethod.POST)
public String order(@RequestParam(value = "name") String name, @RequestParam(value = "age") Integer age) {
    return name+age;
}

curl请求命令

curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/plain' 'http://localhost:8111/order/form/data?name=wangwu&age=22'
#和get请求命令不同,使用post请求使用,默认Content-Type: application/json

前面curl命令是swagger打印的,如果使用x-www-form-urlencode,测试如下

curl -X POST --header 'Content-Type: x-www-form-urlencode' --header 'Accept: text/plain' 'http://localhost:8111/order/form/data?name=wangwu&age=22'
#这样也是可以的

image-20201214211515630

postman请求和console结果如下,没有content-type,说明使用的是x-www-form-urlencode方式,如下图

image-20201214211757779

顶顶顶顶顶顶顶顶顶顶

case3.请求参数在http body

case3.1.content-type=multipart/form-data方式

测试代码如下

@RequestMapping(value = "/order/form", method = RequestMethod.POST)
public String order(@RequestParam(value = "name") String name, @RequestParam(value = "age") Integer age) {
    return name+age;
}

postman请求以及console如下

image-20201214211949406

case3.2.content-type=x-www-form-urlencode方式

测试代码如下

@RequestMapping(value = "/order/form", method = RequestMethod.POST)
public String order(@RequestParam(value = "name") String name, @RequestParam(value = "age") Integer age) {
    return name+age;
}

postman请求以及console如下

image-20201214212217558

发现@RequestParam是可以解析POST content-type是x-www-form-urlencode的http body ,GET请求不行。

case3.3.content-type=application/json方式

测试代码如下

@RequestMapping(value = "/order/json", method = RequestMethod.POST)
public String orderJson(@RequestBody  TestParam param) {
    return JSON.toJSONString(param);
}

@Data
public class TestParam {
	private Integer age;
	private String name;
}

postman请求以及console如图,请求content-type是application/json

image-20201214212626066

http请求和mvc解析总结如下

Content-TypeContent-Length适用请求
路径在url上x-www-form-urlencodeGET、POST@PathVariable
请求参数拼接在urlx-www-form-urlencodeGET、POST@RequestParam
请求参数在http bodyx-www-form-urlencodeGET@RequestParam
multipart/form-dataGET、POST@RequestParam
application/jsonGET、POST@RequestBody

http报文解析流程说明:

http协议,实际底层使用的是tcp协议,在上面封装了http格式的报文(所谓协议就是一种数据格式,发送方和接收方约定以此 格式编码和解码报文),http报文格式如下

image-20201215185025579

http请求解析过程大概如下:
客户端(浏览器)通过socket和服务端(tomcat)建立tcp连接,客户端发送http请求,数据格式就是图中,服务端接收到数据先解析请求行、再解析请求头、最后解析请求体

解析请求行:解析第一个空格前面数据作为请求方式,接着解析第二个空格前面作为url,接着解析第二个空格后面作为HTTP/1.1,接着是CRLF说明请求行解析完毕,接着下面解析请求头

解析请求头:解析冒号前面的作为key,冒号后面的作为value,接着是CRLF,如果连续两个CRLF,则说明该header是最后一个header,那么接着就要解析http body。

解析请求体:如果http header内存在Content-Length的时候,说明http body才存在,根据从网络输入流读取Content-Length长度的数据,然后按照http header内的字符集进行解码(解码具体就是java.lang.String.String(byte[], Charset))。

因此对于有http body的时候,http header必须有Content-Length,不然,怎么知道要从网络输入流读取多少字节作为http body呢

http报文是什么时候被解析的?以及存放在哪个对象属性上?

http报文解析逻辑在org.apache.coyote.http11.Http11Processor.service(SocketWrapperBase<?>)方法内,解析请求行和请求头代码如下:

image-20201216135922227

首先http客户端和服务端连接连接,通过socket发送数据到服务端后,服务端接收到的数据就存储在缓冲区org.apache.coyote.http11.Http11InputBuffer对象上(严格来说是存放在org.apache.coyote.http11.Http11InputBuffer.byteBuffer属性上,这个属性是java.nio.ByteBuffer),此时的数据还是二进制流,并未被解析,要使用数据,必须要对数据解析,把二进制数据解析为我们可视数据,接着就是在parseRequestLine解析请求行,在parseHeaders解析请求头(http header)。

那么解析后的数据存储到org.apache.coyote.Request,该对象并非HttpRequest,而是tomcat定义的一个http数据载体,用于网络交互。

请求参数拼接在在url后面,x-www-form-urlencode方式

比如请求行GET /order/form?name=zhangsan&age=20 HTTP/1.1

org.apache.coyote.Request.methodMB 存放解析http请求行的method,比如例子中的GET
org.apache.coyote.Request.uriMB 存放解析http请求行uri,比如例子中的/order/form
org.apache.coyote.Request.queryMB 存放解析http请求行的请求参数,比如例子中的zhangsan&age=20
org.apache.coyote.Request.protoMB 存放解析http请求行的版本,比如例子中的HTTP/1.1

parseHeaders方法解析请求头(http header)时候,按照规则把解析到的header的key value保存在org.apache.coyote.http11.Http11InputBuffer.headers,而这个对象是和org.apache.coyote.Request.headers是一个对象,因此就把header解析到了org.apache.coyote.Request上。

image-20201216145332512

此时解析的请求参数应用程序并不能通过request.getParameter(String)来使用,这个方法是从org.apache.coyote.Request.parameters对象上获取参数的(具体就是org.apache.tomcat.util.http.Parameters.paramHashValues这个map),那么是什么时候解析到org.apache.tomcat.util.http.Parameters的呢?

解析请求参数到Parameters.paramHashValues是在方法org.apache.catalina.connector.Request.parseParameters()内进行解析到该map的,通过key value规则把请求参数解析到paramHashValues这个map上(注意:是把org.apache.tomcat.util.http.Parameters.queryMB上的查询参数串解析到map上,而org.apache.tomcat.util.http.Parameters.queryMB和org.apache.coyote.Request.queryMB是同一个对象,在解析http请求行的时候就已经存在了)。那么该方法是什么时候被调用的呢?在org.apache.catalina.connector.Request.getParameterValues(String)方法内,如果未被解析过,则执行解析请求参数,代码如图:

image-20201216150616753

org.apache.catalina.connector.Request.getParameterValues(String)是在spring mvc的@RequestParam获取绑定参数的值时候,调用的,执行堆栈如下:

image-20201216152325635

图中的@1处就是根据@RequestParam指定的参数名称从HttpRequest获取参数值,具体就是从org.apache.coyote.Request获取参数值(最终是从org.apache.tomcat.util.http.Parameters.paramHashValues这个map上获取)。

图中@2是@RequestParam获取参数名称

图中@3是根据请求的uri找到对应的Controller mapping方法执行

因此一个简单的request.getParameter(String)执行获取请求参数值就明白了,具体图如下:

未命名文件 (1)

从上面分析可知,请求参数在url上,如何获取解析url上的请求参数,那么如果请求是json,数据在http body上送的方式呢?

请求方式是application/json

json报文格式,对应mvc接收都是使用的@RequestBody,这个注解的具体解析类是RequestResponseBodyMethodProcessor。

我分析的思路是这样的,http客户端通过socket已经把二进制数据都传输完了,二进制数据都保存在org.apache.coyote.Request.inputBuffer对象上(实际就是jdk的字节缓冲区ByteBuffer),那么无论何时读取http body数据,都是要从org.apache.coyote.Request.inputBuffer对象上读取,那么就看哪里调用了org.apache.coyote.Request.inputBuffer对象,如下图

image-20201216154748834

从经验和字面上分析,应该是doRead方法,把断点打在该方法,果然是这里,堆栈如下图:

image-20201216155042800

接着通过MappingJackson2HttpMessageConverter(具体执行方法是MappingJackson2HttpMessageConverter.read(Type, Class<?>, HttpInputMessage) )把二进制流数据转换为json格式数据。说明json请求方式,http body报文并不会保存在org.apache.coyote.Request,org.apache.coyote.Request保存的只是二进制流。

请求是POST,x-www-form-urlencode方式,请求数据在http body

如下图

image-20201216163855335

对应的java Controller代码

@RequestMapping(value = "/order/form", method = RequestMethod.POST)
public String order(@RequestParam(value = "name") String name, @RequestParam(value = "age") Integer age) {
    return name+age;
}

这种情况,请求数据在http body内,经过分析,请求数据也是解析到org.apache.coyote.Request.parameters对象(即org.apache.tomcat.util.http.Parameters.paramHashValues这个map上),和请求参数拼接在url上一样。那么什么时候调用去解析请求参数到paramHashValues的呢?只要是第一次调用org.apache.catalina.connector.Request.getParameterXXX方法的时候,判断请求参数未解析,就会去解析参数到paramHashValues。

总结

对于Content-Type为x-www-form-urlencode,GET和POST,请求数据在http body还是拼接在url后面,最终解析后的请求参数都在org.apache.coyote.Request.parameters对象(即org.apache.tomcat.util.http.Parameters.paramHashValues这个map上),可以使用request.getParameter(String)来获取请求参数值。

对于Content-Type为application/json,无论GET还是POST,请求数据只能在http body,使用@RequestBody进行解析为java bean,解析后的请求数据并不保存到org.apache.coyote.Request对象。

最后,写这个笔记有什么用呢?是因为生产曾经遇到一个bug,我们需要把jenkins构建日志保存到fastdfs,从而供用户查看构建日志,具体流程如下:

image-20201216172539798

有用户反馈有时候查看到的构建日志不完整,发现这些不完整的构建日志到fastdfs中也是不完整的,这种情况发生在前端项目构建日志,而且基本出现在前端的构建日志通常都比较大(有些200k)的时候,请求是POST x-www-form-urlencode,刚开始排查,以为是超过了http请求的最大限制,经过测试500k的数据发送,发现是没有问题。代码如下:

public void uploadConsoleOut(BuildWithDetails details, Long buildId) {
		try {
				String consoleOutputText = details.getConsoleOutputText();//获取jenkins构建日志,jenkins console log
				Map<String, Object> map = new HashMap<>(2);
				map.put("file", consoleText);
				map.put("pathValue", "buildLog-" + buildLogId);
				String post= post=HttpUtil.post(url, map);//url是包管理系统接口,HttpUtil是Hutool工具类
				//其它省略
			}
		} catch (IOException e) {
    		//只打印异常
			log.error("upload buildlog occurs exception", e);
		}
    }

继而想起了x-www-form-urlencode时候,请求参数是保存在org.apache.coyote.Request.parameters对象,用出现问题的这个构建日志本地做测试,发现org.apache.coyote.Request.parameters对象的参数key不仅有file和pathValue,还有其它一大堆,此时才明白,可能是构建日志中有&符号导致http解析的时候还有其它key,我们以为格式是file=[构建日志内容]&pathValue=buildLog-5685,但是实际构建日志内容有&,实际是file=[构建日志内容一部分]&构建日志另一部分=构建日志其它部分&pathValue=buildLog-5685,这样就导致包管理系统获取file的时候只获取到了构建日志中第一个&符号之前的日志数据,剩余就没了,最终导致用户只查看到部分构建日志。解决办法,改用json,修复后代码如下,问题解决。

public void uploadConsoleOut(BuildWithDetails details, Long buildId) {
		try {
				String consoleOutputText = details.getConsoleOutputText();//获取jenkins构建日志,jenkins console log
				Map<String, Object> map = new HashMap<>(2);
				map.put("file", consoleText);
				map.put("pathValue", "buildLog-" + buildLogId);
            	String jsonStr = JSONUtil.toJsonStr(map);
				String post= post=HttpUtil.post(url, jsonStr);//修复后hutool对于json串会采用content-type为application/json
				//其它省略
			}
		} catch (IOException e) {
    		//只打印异常
			log.error("upload buildlog occurs exception", e);
		}
    }

第一次出现这个问题解决了,由于没记录且间隔了几个月,又遇到了发布系统保存执行日志到fastdfs也是同样问题,执行日志中有&导致执行日志打印不完整,当时以为shell问题,后来解决半天才想起来是这个问题,还是记录下为好。

@Controller & @ExceptionHander

通常我们的web项目都会有个全局的异常处理器,都是使用@Controller和@ExceptionHander。

ControllerAdvice拆开来就是Controller Advice,Advice在Spring的AOP中,是用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ControllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行切面环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。但是Controller的具体实现,并不是通过AOP功能实现的。

@ControllerAdvice使用AOP思想可以这么理解:此注解对目标Controller的通知是个环绕通知,织入的方式是注解方式,增强器是注解标注的方法。如此就很好理解@ControllerAdvice搭配@InitBinder/@ModelAttribute/@ExceptionHandler起到的效果喽~

@ControllerAdvice是在类上声明的注解,其用法主要有三点:
1.结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的。 基本web项目都用到。
2.结合方法型注解@InitBinder,用来设置WebDataBinder,WebDataBinder自动绑定前台请求参数到model中。
3.结合方法型注解@ModelAttribute,让全局的@RequestMapping都能获得在此处设置的键值对,即在目标Controller方法之前执行。

测试代码如下

@ControllerAdvice
public class GloableException {
    private final static Logger logger = LoggerFactory.getLogger(GloableException.class);
    @ExceptionHandler({Exception.class})
    @ResponseBody
    @ResponseStatus(value = HttpStatus.OK)
    public CommonResult handleControllerException(Exception e, WebRequest request, HttpServletResponse response) {
        logger.error(e.getMessage(), e);
        if (e instanceof BussinessException) {
            return CommonResult.failed("业务异常!");
        }
        return CommonResult.failed("系统异常:"+e.getMessage());
    }
}

public class BussinessException extends RuntimeException{
	public BussinessException(String msg) {
        super(msg);
    }
}

@RestController
public class IndexController {
	@RequestMapping(value = "/order/form", method = RequestMethod.GET)
	public String order(@RequestParam(value = "name") String name, @RequestParam(value = "age") Integer age) {
		if (true) {
			throw new BussinessException("测试参数异常!");
		}
		return name+age;
	}
}


测试结果

{
    "code": 500,
    "message": "业务异常!",
    "data": null
}

源码解析

@ControllerAdvice和@ExceptionHandler是在哪里解析的呢?

以前一直从@ControllerAdvice字面意思理解为是通过AOP来实现的,发现实际却不是,因此看了下这块源码

@ControllerAdvice被注解,说明也是个bean。

debug代码,关键处在org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#getExceptionHandlerMethod(HandlerMethod, Exception)方法,执行堆栈如下

image-20201214230832767

getExceptionHandlerMethod关键代码解析

/*
* 查找匹配的全局异常处理方法
*/
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
			ControllerAdviceBean advice = entry.getKey();
			if (advice.isApplicableToBeanType(handlerType)) {//如果handlerType被@ControllerAdvice注解,handlerType是controller的clazz
				ExceptionHandlerMethodResolver resolver = entry.getValue();//异常处理解析器,表示的是@Controller内的@ExceptionHandler
				Method method = resolver.resolveMethod(exception);//根据异常类型,从缓存ExceptionHandlerMethodResolver.mappedMethods获取到匹配的@ExceptionHandler注解的方法
				if (method != null) {
					return new ServletInvocableHandlerMethod(advice.resolveBean(), method);//包装为ServletInvocableHandlerMethod,参数分别是@Controller bean, @ExceptionHandler注解方法
				}
			}
		}

那么this.exceptionHandlerAdviceCache的内容是怎么来的呢?

org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.initExceptionHandlerAdviceCache()方法内

private void initExceptionHandlerAdviceCache() {
		if (getApplicationContext() == null) {
			return;
		}
		//查找IOC容器内的被@ControllerAdvice注解的bean
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);

		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			if (beanType == null) {
				throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
			}
            //获取@ControllerAdvice注解的bean被@ExceptionHandler注解方法,最终把这些方法缓存到ExceptionHandlerMethodResolver.mappedMethods
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
			if (resolver.hasExceptionMappings()) {
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);//把被@ControllerAdvice注解的bean缓存
			}
			if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
				this.responseBodyAdvice.add(adviceBean);
			}
		}
	}

获取到异常处理方法后,通过反射调用异常处理方法,具体逻辑是在org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(HttpServletRequest, HttpServletResponse, HandlerMethod, Exception)

image-20201215000751523

具体执行堆栈记录如下

GloableException.handleControllerException(Exception, WebRequest, HttpServletResponse) line: 26	
GeneratedMethodAccessor62.invoke(Object, Object[]) line: not available	
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 45005	
Method.invoke(Object, Object...) line: 498	
ServletInvocableHandlerMethod(InvocableHandlerMethod).doInvoke(Object...) line: 189	
ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 138	
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 102	
ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(HttpServletRequest, HttpServletResponse, HandlerMethod, Exception) line: 412	
ExceptionHandlerExceptionResolver(AbstractHandlerMethodExceptionResolver).doResolveException(HttpServletRequest, HttpServletResponse, Object, Exception) line: 61	
ExceptionHandlerExceptionResolver(AbstractHandlerExceptionResolver).resolveException(HttpServletRequest, HttpServletResponse, Object, Exception) line: 139	
HandlerExceptionResolverComposite.resolveException(HttpServletRequest, HttpServletResponse, Object, Exception) line: 80	
DispatcherServlet.processHandlerException(HttpServletRequest, HttpServletResponse, Object, Exception) line: 1297	
DispatcherServlet.processDispatchResult(HttpServletRequest, HttpServletResponse, HandlerExecutionChain, ModelAndView, Exception) line: 1109	
DispatcherServlet.doDispatch(HttpServletRequest, HttpServletResponse) line: 1055	

springboot是如何跳转到/error接口的疑问?

通常我们的应用抛出了异常且没被@HandlerException捕获处理的话,页面会出现这个错误

image-20201215104408864

这个具体是怎么来的呢?

springboot有个ErrorController接口,默认是异常处理的入口,默认会创建controller bean BasicErrorController,如果程序抛出的异常没被捕获,则会执行到该方法的/error内(/error的来源是org.springframework.boot.autoconfigure.web.ErrorProperties.path),

具体执行流程如下:

tomcat的执行入口是CoyoteAdapter.service(Request, Response),接着执行tomcat的pipeline,继而执行FilterChain,然后是Servlet,具体如图

image-20201215151232083

其中tomcat pipeline是[StandardEngineValve, ErrorReportValve, StandardHostValve, NonLoginAuthenticator, StandardContextValve, StandardWrapperValve],执行方式和filterchain类似,也是串行执行。

分几种情况来说:

代码@2处,如果没有抛出异常,不进入异常处理,返回的http status code 200。

代码@2处抛出异常,如果在代码@5处,这个异常被吞了,则返回的http status code 200,通常web项目这样是捕捉到异常进行统一处理,比如下面代码,异常就被吞了。

@ControllerAdvice
public class GloableException {
    private final static Logger logger = LoggerFactory.getLogger(GloableException.class);
    @ExceptionHandler({Exception.class})
    @ResponseBody
    @ResponseStatus(value = HttpStatus.OK)
    public CommonResult handleControllerException(Exception e, WebRequest request, HttpServletResponse response) throws Exception {
        logger.error(e.getMessage(), e);
        if (e instanceof BussinessException) {
            return CommonResult.failed("业务异常!");
        }
        return CommonResult.failed("系统异常:"+e.getMessage());
    }
}

代码@5处,如果抛出了异常呢?有人问,这里本来就是为了做异常统一处理,为什么要要抛出异常呢?比如健康检查失败,就要返回http status code非200错误,按照上面做法,异常被吞了。比如用发布系统发布,curl health-url非200就任务发布失败,还有发布到k8s容器内也可以用健康url作为个心跳探测,如果连续10次curl返回http status code非200,则认为应用宕机了,会自动重启应用。因此这样情况是有场景的。代码如下:

@ControllerAdvice
public class GloableException {
    private final static Logger logger = LoggerFactory.getLogger(GloableException.class);
    @ExceptionHandler({Exception.class})
    @ResponseBody
    @ResponseStatus(value = HttpStatus.OK)
    public CommonResult handleControllerException(Exception e, WebRequest request, HttpServletResponse response) throws Exception {
        logger.error(e.getMessage(), e);
        if (e instanceof BussinessException) {
            return CommonResult.failed("业务异常!");
        } else if (e instanceof HealthException) {//健康检查失败抛出HealthException,返回500错误
        	throw e;
        }
        return CommonResult.failed("系统异常:"+e.getMessage());
    }
}

这样就是在代码@5处抛出了异常,异常一层层的向上throw异常,最终是执行tomcat pipeline的StandardHostValve.invoke(Request, Response)抛出ServletException异常被捕获并吞了,执行org.apache.catalina.core.StandardWrapperValve.exception(Request, Response, Throwable),把http status code由200改为500。这段代码如下

private void exception(Request request, Response response,
                           Throwable exception) {
    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);//把异常保存到request,在StandardHostValve内就可以取出来了
    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);//把http status code改为500
    response.setError();//把org.apache.coyote.Response.errorState改为1,这样在StandardHostValve内就可以forward到/error接口
}

StandardHostValve执行完tomcat pipeline后,由于springboot默认加了个/error Controller接口供http status code非200的时候调用,因此会根据异常forward到/error接口,代码截图解释如下:

image-20201215173058676

org.apache.catalina.core.StandardHostValve.custom(Request, Response, ErrorPage)代码截图如下

image-20201215173227927

forward后就去执行Servlet的service功能,即DispatcherServlet.doDispatch(HttpServletRequest, HttpServletResponse),找到/error的对应Controller方法执行,最终进入执行org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(HttpServletRequest)。

我们在项目中,可以重写ErrorController来覆盖springboot默认的BasicErrorController。

最近给一个项目写健康检查接口用于发布检查,在抛出异常后发现返回的http status code还是200,经过排查发现项目重写了ErrorController,把健康检查抛出的异常的请求响应http status code给了200。

@Controller
@RequestMapping(value = "/error")
public class CommonErrorController implements ErrorController {
    protected Map<String, Object> getErrorAttributes(HttpServletRequest request) {
    	DispatcherServletWebRequest requestAttributes = new DispatcherServletWebRequest(request);
        ErrorAttributes errorAttributes = new DefaultErrorAttributes();
        return errorAttributes.getErrorAttributes(requestAttributes, false);
    }
    @RequestMapping
    @ResponseBody
    public Result error(HttpServletRequest request, HttpServletResponse response) {
        // set CORS header
        response.setStatus(200);//给请求都返回200,因此健康检查抛出异常也不会返回500
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", "GET, POST");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "content-type");
        response.setHeader("Access-Control-Allow-Credentials","true"); //是否支持cookie跨应用
        
        Map<String, Object> errorAttributes = getErrorAttributes(request);
        int code = Integer.parseInt(String.valueOf(errorAttributes.get("status")));
        String key = String.valueOf(errorAttributes.get("error"));
        String message = String.valueOf(errorAttributes.get("message"));

        return CommonResult.failed(message);
    }
    @Override
    public String getErrorPath() {
        return "/error";
    }
}

具体跳转到/error的执行堆栈如下

BasicErrorController.error(HttpServletRequest) line: 98	
ServletInvocableHandlerMethod(InvocableHandlerMethod).doInvoke(Object...) line: 189	
ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 138	
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 102	
RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 895	
RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 800	
RequestMappingHandlerAdapter(AbstractHandlerMethodAdapter).handle(HttpServletRequest, HttpServletResponse, Object) line: 87	
DispatcherServlet.doDispatch(HttpServletRequest, HttpServletResponse) line: 1038	
DispatcherServlet.doService(HttpServletRequest, HttpServletResponse) line: 942	
DispatcherServlet(FrameworkServlet).processRequest(HttpServletRequest, HttpServletResponse) line: 1005	
DispatcherServlet(FrameworkServlet).doGet(HttpServletRequest, HttpServletResponse) line: 897	
DispatcherServlet(HttpServlet).service(HttpServletRequest, HttpServletResponse) line: 634	
DispatcherServlet(FrameworkServlet).service(HttpServletRequest, HttpServletResponse) line: 882	
DispatcherServlet(HttpServlet).service(ServletRequest, ServletResponse) line: 741	
ApplicationFilterChain.internalDoFilter(ServletRequest, ServletResponse) line: 231	
ApplicationFilterChain.doFilter(ServletRequest, ServletResponse) line: 166	
ApplicationDispatcher.invoke(ServletRequest, ServletResponse, ApplicationDispatcher$State) line: 712	
ApplicationDispatcher.processRequest(ServletRequest, ServletResponse, ApplicationDispatcher$State) line: 461	
ApplicationDispatcher.doForward(ServletRequest, ServletResponse) line: 384	
ApplicationDispatcher.forward(ServletRequest, ServletResponse) line: 312	
StandardHostValve.custom(Request, Response, ErrorPage) line: 394	
StandardHostValve.status(Request, Response) line: 253	
StandardHostValve.throwable(Request, Response, Throwable) line: 348	
StandardHostValve.invoke(Request, Response) line: 173	

写着篇记录,记录了下通用的全局异常处理,以及如何控制http status code的显示,对于整个http请求的处理也再加深下印象,便于工作中排查问题。

redirct VS forward

ResponseBodyAdvice & RequestBodyAdvice

对入参和出参进行解密加密需求

实际开发中有这样需求,需要对所有的入参和出参进行解密和加密,这样功能和业务无关,肯定不能在每个Controller写,需要抽取为公共处理,最好是业务无感。有人说可以使用mvc拦截器实现,我测试分析了下,使用拦截器是无法实现的。

使用拦截器方案分析

先来看下mvc的拦截器执行流程和http二进制流数据如何通过@RequestBody转换为java bean的过程

image-20201218192915312

@1:每个http请求都要由DispatcherServlet这个Servlet接入进行处理,是个总入口,这个DispatcherServlet这个Servlet是注册到了tomcat的,由tomcat进行调用。

@2:根据请求的uri从DispatcherServlet.handlerMappings缓存中获取待执行的Controller的mapping方法。这些mapping方法在spring启动时候注册到handlerMappings。

@3:执行拦截器的前置处理方法,此时http body的数据(业务数据)还保存在tomcat的输入流内,还是二进制数据状态。由于此时不知道数据的结构,是无法进行把二进制流解析为java bean的。

@4:把http body二进制流数据按照@RequestBody指定的java bean格式进行解码

@5:反射调用,执行具体的业务Controller mapping方法

@6:通过@ResponseBody把java bean对象编码为二进制流,组装到http body

@7:执行拦截器的后置处理方法。此时业务数据也已经是二进制流数据了

@8:处理Controller mapping的执行结果,执行结果正常or异常的不同处理方式

@9:执行拦截器的收尾方法

通过上面的分析,发现使用拦截器是无法实现对json字符串进行加解密。那么可以考虑在编解码处对数据进行解密和加密。

重写HttpMessageConverter支持加解密

springMVC内置了几种HttpMessageConverter用于消息转换,其中MappingJackson2HttpMessageConverter是用于二进制流数据和java bean之间的转换。那么我们只需要在MappingJackson2HttpMessageConverter上加上加解密功能即可。自己写的时候在ObjectMapper的读写上卡壳,后来发现了这位大佬的已经有具体实现https://www.throwx.cn/2019/11/29/spring-mvc-param-global-encryption-decryption-in-action/。

重写MappingJackson2HttpMessageConverter后,需要把内置的MappingJackson2HttpMessageConverter给移除,需要实现WebMvcConfigurer的extendMessageConverters方法,把默认的MappingJackson2HttpMessageConverter给移除。

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

	/**
	 * 移除内置的MappingJackson2HttpMessageConverter
	 */
	@Override
	public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
		converters.forEach(converter -> {
			if (converter instanceof MappingJackson2HttpMessageConverter) {
				converters.remove(converter);
			}
		});
	}
	
	/**
	 * 添加自定义的MappingJackson2HttpMessageConverter
	 */
	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		converters.add(customMappingJackson2HttpMessageConverter());
	}
	
	@Bean
	public CustomMappingJackson2HttpMessageConverter customMappingJackson2HttpMessageConverter() {
		return new CustomMappingJackson2HttpMessageConverter();
	}
}

使用RequestBodyAdvice & ResponseBodyAdvice

mvc在使用消息转换器进行消息转换前后,会去执行被@ControllerAdvice注解的RequestBodyAdvice实现,具体代码如图

image-20201218231145703

image-20201218231512225

分别写个RquestBodyAdvice、ResponseBodyAdvice的实现类,并且用@ControllerAdvice注解,在RquestBodyAdvice的afterBodyRead写解密,在ResponseBodyAdvice的beforeBodyWrite写加密逻辑即可,比实现消息转换器简单了好多。

标签:http,springboot,form,零碎,apache,org,解析,请求
来源: https://blog.csdn.net/yulewo123/article/details/116610817