热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

http和springboot的一些零碎记录

目录HTTP提交数据的方式总结GET请求case1.请求路径在url上case2.请求参数拼接在url后面case3.请求参数在httpbodycase3.1.content-ty

目录
  • HTTP提交数据的方式总结
  • GET请求
    • case1.请求路径在url上
    • case2.请求参数拼接在url后面
    • case3.请求参数在http body
      • case3.1.content-type=multipart/form-data方式
      • case3.2.content-type=x-www-form-urlencode方式
      • case3.3.content-type=application/json方式
  • POST请求
    • case1.请求路径在url上
    • case2.请求参数拼接在url后面
    • case3.请求参数在http body
      • case3.1.content-type=multipart/form-data方式
      • case3.2.content-type=x-www-form-urlencode方式
      • case3.3.content-type=application/json方式
  • http请求和mvc解析总结如下
  • http报文解析流程说明:
  • http报文是什么时候被解析的?以及存放在哪个对象属性上?
    • 请求参数拼接在在url后面,x-www-form-urlencode方式
    • 请求方式是application/json
    • 请求是POST,x-www-form-urlencode方式,请求数据在http body
    • 总结
  • @Controller & @ExceptionHander
    • 测试代码如下
    • 源码解析
  • springboot是如何跳转到/error接口的疑问?
  • ResponseBodyAdvice & RequestBodyAdvice
    • 对入参和出参进行解密加密需求
    • 使用拦截器方案分析
    • 重写HttpMessageConverter支持加解密
    • 使用RequestBodyAdvice & ResponseBodyAdvice

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即可,其它不需要输入。

http和springboot的一些零碎记录

通过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末尾,

http和springboot的一些零碎记录

在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

http和springboot的一些零碎记录

发现竟然请求成功了。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方式,发现请求报错

http和springboot的一些零碎记录

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

http和springboot的一些零碎记录

为什么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。

http和springboot的一些零碎记录

postman console如下

http和springboot的一些零碎记录

postman console如下

http和springboot的一些零碎记录

翻车了,感觉和之前理解的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

http和springboot的一些零碎记录

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'
#这样也是可以的

http和springboot的一些零碎记录

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

http和springboot的一些零碎记录

顶顶顶顶顶顶顶顶顶顶

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如下

http和springboot的一些零碎记录

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如下

http和springboot的一些零碎记录

发现@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

http和springboot的一些零碎记录

http请求和mvc解析总结如下

Content-Type Content-Length 适用请求
路径在url上 x-www-form-urlencode GET、POST @PathVariable
请求参数拼接在url x-www-form-urlencode GET、POST @RequestParam
请求参数在http body x-www-form-urlencode GET @RequestParam
multipart/form-data GET、POST @RequestParam
application/json GET、POST @RequestBody

http报文解析流程说明:

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

http和springboot的一些零碎记录

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)方法内,解析请求行和请求头代码如下:

http和springboot的一些零碎记录

首先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上。

http和springboot的一些零碎记录

此时解析的请求参数应用程序并不能通过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)方法内,如果未被解析过,则执行解析请求参数,代码如图:

http和springboot的一些零碎记录

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

http和springboot的一些零碎记录

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

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

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

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

http和springboot的一些零碎记录

从上面分析可知,请求参数在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对象,如下图

http和springboot的一些零碎记录

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

http和springboot的一些零碎记录

接着通过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

如下图

http和springboot的一些零碎记录

对应的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,从而供用户查看构建日志,具体流程如下:

http和springboot的一些零碎记录

有用户反馈有时候查看到的构建日志不完整,发现这些不完整的构建日志到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 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 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)方法,执行堆栈如下

http和springboot的一些零碎记录

getExceptionHandlerMethod关键代码解析

/*
* 查找匹配的全局异常处理方法
*/
for (Map.Entry 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 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)

http和springboot的一些零碎记录

具体执行堆栈记录如下

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捕获处理的话,页面会出现这个错误

http和springboot的一些零碎记录

这个具体是怎么来的呢?

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

具体执行流程如下:

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

http和springboot的一些零碎记录

其中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接口,代码截图解释如下:

http和springboot的一些零碎记录

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

http和springboot的一些零碎记录

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 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 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的过程

http和springboot的一些零碎记录

@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> converters) {
		converters.forEach(converter -> {
			if (converter instanceof MappingJackson2HttpMessageConverter) {
				converters.remove(converter);
			}
		});
	}
	
	/**
	 * 添加自定义的MappingJackson2HttpMessageConverter
	 */
	@Override
	public void configureMessageConverters(List> converters) {
		converters.add(customMappingJackson2HttpMessageConverter());
	}
	
	@Bean
	public CustomMappingJackson2HttpMessageConverter customMappingJackson2HttpMessageConverter() {
		return new CustomMappingJackson2HttpMessageConverter();
	}
}

使用RequestBodyAdvice & ResponseBodyAdvice

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

http和springboot的一些零碎记录

http和springboot的一些零碎记录

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


推荐阅读
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • Spring框架中枚举参数的正确使用方法与技巧
    本文详细阐述了在Spring Boot框架中正确使用枚举参数的方法与技巧,旨在帮助开发者更高效地掌握和应用枚举类型的数据传递,适合对Spring Boot感兴趣的读者深入学习。 ... [详细]
  • 利用爬虫技术抓取数据,结合Fiddler与Postman在Chrome中的应用优化提交流程
    本文探讨了如何利用爬虫技术抓取目标网站的数据,并结合Fiddler和Postman工具在Chrome浏览器中的应用,优化数据提交流程。通过详细的抓包分析和模拟提交,有效提升了数据抓取的效率和准确性。此外,文章还介绍了如何使用这些工具进行调试和优化,为开发者提供了实用的操作指南。 ... [详细]
  • Java能否直接通过HTTP将字节流绕过HEAP写入SD卡? ... [详细]
  • 本文深入解析了Java面向对象编程的核心概念及其应用,重点探讨了面向对象的三大特性:封装、继承和多态。封装确保了数据的安全性和代码的可维护性;继承支持代码的重用和扩展;多态则增强了程序的灵活性和可扩展性。通过具体示例,文章详细阐述了这些特性在实际开发中的应用和优势。 ... [详细]
  • 在探讨Hibernate框架的高级特性时,缓存机制和懒加载策略是提升数据操作效率的关键要素。缓存策略能够显著减少数据库访问次数,从而提高应用性能,特别是在处理频繁访问的数据时。Hibernate提供了多层次的缓存支持,包括一级缓存和二级缓存,以满足不同场景下的需求。懒加载策略则通过按需加载关联对象,进一步优化了资源利用和响应时间。本文将深入分析这些机制的实现原理及其最佳实践。 ... [详细]
  • 如何在PHP中准确获取服务器IP地址?
    如何在PHP中准确获取服务器IP地址? ... [详细]
  • 全面解析JavaScript代码注释技巧与标准规范
    在Web前端开发中,JavaScript代码的可读性和维护性至关重要。本文将详细介绍如何有效地使用注释来提高代码的可读性,并探讨JavaScript代码注释的最佳实践和标准规范。通过合理的注释,开发者可以更好地理解和维护复杂的代码逻辑,提升团队协作效率。 ... [详细]
  • 本文介绍了如何利用Struts1框架构建一个简易的四则运算计算器。通过采用DispatchAction来处理不同类型的计算请求,并使用动态Form来优化开发流程,确保代码的简洁性和可维护性。同时,系统提供了用户友好的错误提示,以增强用户体验。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 本文详细探讨了 jQuery 中 `ajaxSubmit` 方法的使用技巧及其应用场景。首先,介绍了如何正确引入必要的脚本文件,如 `jquery.form.js` 和 `jquery-1.8.0.min.js`。接着,通过具体示例展示了如何利用 `ajaxSubmit` 方法实现表单的异步提交,包括数据的发送、接收和处理。此外,还讨论了该方法在不同场景下的应用,如文件上传、表单验证和动态更新页面内容等,提供了丰富的代码示例和最佳实践建议。 ... [详细]
  • Squaretest:自动生成功能测试代码的高效插件
    本文将介绍一款名为Squaretest的高效插件,该工具能够自动生成功能测试代码。使用这款插件的主要原因是公司近期加强了代码质量的管控,对各项目进行了严格的单元测试评估。Squaretest不仅提高了测试代码的生成效率,还显著提升了代码的质量和可靠性。 ... [详细]
author-avatar
sdr700724
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有