前几天写了篇关于fastjson的文章,[《fastjson很好,但不适合我》](fastjson很好,但不适合我 – ())。里边探讨到关于目标循环引证的序列化问题。作为spring序列化的最大竞品,在评论fastjson的时分肯定要对比一下jackson的。所以我也去测验了一下Jackson在目标循环引证的序列化的功用,然后有了一点意外的小发现,在这儿跟咱们评论一下。

首要还得解释一下,jackson的序列化是怎样跟@ControllerAdvice相关上的呢?
前篇文章里说过,关于目标循环引证的序列化问题,fastjson和jackson别离采取了两种情绪,fastjson是默许处理了,而jackson是默许抛出反常。后者把主动权交给了用户。
已然这儿抛出了反常,就涉及到反常的大局处理,跟业务相同,咱们不行能以硬编码的方式在每个办法里别离处理反常,而是经过一致大局反常处理。


@ControllerAdvice 大局反常捕获

这儿简略的做一下介绍,厌弃烦琐的朋友可直接略过,跳到第2部份。

Spring家族中,经过注解@ControllerAdvice或许 @RestControllerAdvice 即可敞开大局反常处理,运用该注解表明敞开了大局反常的捕获,咱们只需在自界说一个办法运用@ExceptionHandler注解然后界说捕获反常的类型即可对这些捕获的反常进行一致的处理。

只要反常终究能够到达controller层,且与@ExceptionHandler界说反常类型相匹配,就能被捕获。

@RestControllerAdvice
public class GlobalExceptionHandler {
    Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    @ExceptionHandler(value = Exception.class)
    public Result exceptionHandler(Exception e){
        logger.error(e.getMessage(), e);
        return Result.error(e.getMessage());
    }
    @ExceptionHandler(value = RuntimeException.class)
    public Result exceptionHandlerRuntimeException(Exception e){
        logger.error(e.getMessage(), e);
        return Result.error(e.getMessage());
    }
    // 或许其它自界说反常
}

再界说一个一致的接口回来目标:

点击检查代码
public class Result<T> implements Serializable {
    private String code;
    private Boolean success;
    private T data;
    private String msg;
    public Result(String code, Boolean success, String msg) {
        this.code = code;
        this.success = success;
        this.msg = msg;
    }
    public Result(String code, String msg, T data) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }
    public Result() {
        this.code = ReturnCodeEnum.OK.getCode();
        this.success = true;
        this.msg = ReturnCodeEnum.OK.getMsg();
    }
    public void serverFailed() {
        this.serverFailed((Exception)null);
    }
    public void serverFailed(Exception e) {
        this.code = ReturnCodeEnum.SERVER_FAILED.getCode();
        this.success = false;
        if (e == null) {
            this.msg = ReturnCodeEnum.SERVER_FAILED.getMsg();
        } else {
            this.msg = e.getMessage();
        }
    }
    public static <T> Result<T> success(T data) {
        Result<T> success = new Result();
        success.setData(data);
        return success;
    }
    public static <T> Result<T> success() {
        return new Result();
    }
    public static <T> Result<T> error() {
        return new Result(ReturnCodeEnum.SERVER_FAILED.getCode(), false, ReturnCodeEnum.SERVER_FAILED.getMsg());
    }
    public static <T> Result<T> error(String message) {
        return new Result(ReturnCodeEnum.SERVER_FAILED.getCode(), false, message);
    }
    public static <T> Result<T> error(String code, String message) {
        return new Result(code, false, message);
    }
    public void resetWithoutData(Result result) {
        this.success = result.getSuccess();
        this.code = result.getCode();
        this.msg = result.getMsg();
    }
    public void resetResult(ReturnCodeEnum returnCodeEnum, boolean isSuccess) {
        this.code = returnCodeEnum.getCode();
        this.success = isSuccess;
        this.msg = returnCodeEnum.getMsg();
    }
    public static <T> Result<T> error(ReturnCodeEnum returnCodeEnum) {
        Result<T> error = new Result();
        error.code = returnCodeEnum.getCode();
        error.success = false;
        error.msg = returnCodeEnum.getMsg();
        return error;
    }
    public String getCode() {
        return this.code;
    }
    public Boolean getSuccess() {
        return this.success;
    }
    public T getData() {
        return this.data;
    }
    public String getMsg() {
        return this.msg;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public void setSuccess(Boolean success) {
        this.success = success;
    }
    public void setData(T data) {
        this.data = data;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Result)) {
            return false;
        } else {
            Result<?> other = (Result)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                label59: {
                    Object this$code = this.getCode();
                    Object other$code = other.getCode();
                    if (this$code == null) {
                        if (other$code == null) {
                            break label59;
                        }
                    } else if (this$code.equals(other$code)) {
                        break label59;
                    }
                    return false;
                }
                Object this$success = this.getSuccess();
                Object other$success = other.getSuccess();
                if (this$success == null) {
                    if (other$success != null) {
                        return false;
                    }
                } else if (!this$success.equals(other$success)) {
                    return false;
                }
                Object this$data = this.getData();
                Object other$data = other.getData();
                if (this$data == null) {
                    if (other$data != null) {
                        return false;
                    }
                } else if (!this$data.equals(other$data)) {
                    return false;
                }
                Object this$msg = this.getMsg();
                Object other$msg = other.getMsg();
                if (this$msg == null) {
                    if (other$msg != null) {
                        return false;
                    }
                } else if (!this$msg.equals(other$msg)) {
                    return false;
                }
                return true;
            }
        }
    }
    protected boolean canEqual(Object other) {
        return other instanceof Result;
    }
    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $code = this.getCode();
        int result = result * 59 + ($code == null ? 43 : $code.hashCode());
        Object $success = this.getSuccess();
        result = result * 59 + ($success == null ? 43 : $success.hashCode());
        Object $data = this.getData();
        result = result * 59 + ($data == null ? 43 : $data.hashCode());
        Object $msg = this.getMsg();
        result = result * 59 + ($msg == null ? 43 : $msg.hashCode());
        return result;
    }
    public String toString() {
        return "Result(code=" + this.getCode() + ", success=" + this.getSuccess() + ", data=" + this.getData() + ", msg=" + this.getMsg() + ")";
    }
    public Result(String code, Boolean success, T data, String msg) {
        this.code = code;
        this.success = success;
        this.data = data;
        this.msg = msg;
    }

一致状况码:

点击检查代码
public enum ReturnCodeEnum {
    OK("200", "success"),
    OPERATION_FAILED("202", "操作失利"),
    PARAMETER_ERROR("203", "参数过错"),
    UNIMPLEMENTED_INTERFACE_ERROR("204", "未完成的接口"),
    INTERNAL_SYSTEM_ERROR("205", "系统内部过错"),
    THIRD_PARTY_INTERFACE_ERROR("206", "第三方接口过错"),
    CRS_TOKEN_INVALID("401", "token无效"),
    PERMISSIONS_ERROR("402", "业务权限认证失利"),
    AUTHENTICATION_FAILED("403", "登陆超时,请从头登陆"),
    SERVER_FAILED("500", "server failed 500 !!!"),
    DATA_ERROR("10001", "数据获取失利"),
    UPDATE_ERROR("10002", "操作失利"),
    SIGN_ERROR("10010", "签名过错"),
    ACCOUNT_OR_PASSWORD_ERROR("4011", "用户名或暗码过错"),
    ILLEGAL_PERMISSION("405", "权限不足"),
    FORBIDDON("410", "已被制止"),
    TOKEN_TIME_OUT("4012", "session过期,需从头登录");
    private String code;
    private String msg;
    public String getCode() {
        return this.code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getMsg() {
        return this.msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    private ReturnCodeEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

再界说一个测验目标:

@Getter
@Setter
//@ToString
//@AllArgsConstructor
//@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;
    private Person father;
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

写一个测验接口,模仿循环依靠的目标,运用fastjson进行序列化回来。

public Result test2 (){
        List<Person> list = new ArrayList<>();
        Person obj1 = new Person("张三", 48);
        Person obj2 = new Person("李四", 23);
        obj1.setFather(obj2);
        obj2.setFather(obj1);
        list.add(obj1);
        list.add(obj2);
        Person obj3 = new Person("王麻子", 17);
        list.add(obj3);
        List<Person> young = list.stream().filter(e -> e.getAge() <= 45).collect(Collectors.toList());
        List<Person> children =  list.stream().filter(e -> e.getAge()< 18).collect(Collectors.toList());
        HashMap map = new HashMap();
        map.put("young", young);
        map.put("children", children);
        return Result.success(map);
    }

敞开fastjson的SerializerFeature.DisableCircularReferenceDetect禁用循环依靠检测,使其抛出反常。
访问测验接口,后台打印日志

ERROR 21360 [http-nio-8657-exec-1] [com.nyp.test.config.GlobalExceptionHandler] : Handler dispatch failed; nested exception is java.lang.StackOverflowError

接口回来

{
	"code":"500",
	"data":null,
	"msg":"Handler dispatch failed; nested exception is java.lang.StackOverflowError",
	"success":false
}

证明反常在大局反常捕获处被成功捕获。且回来了500状况码,证明服务端呈现了反常。

jackson的问题

咱们现在换掉fastjson,运用springboot自带的jackson进行序列化。同样仍是上面的代码。 后台打印了日志:

[2023-04-01 15:27:42.230] ERROR 17156 [http-nio-8657-exec-2] [com.nyp.test.config.GlobalExceptionHandler] : Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.nyp.test.model.Person["father"]->com.nyp.test.model.Person["father"]....

日志信息略有不同,是两种不同的序列化结构的差异,总归大局反常捕获也成功了。

再来看回来的成果如下:

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

这就很明显不对劲,后台现已抛出反常,并成功捕获了反常,前端怎样还接纳到了200状况码呢?而且 data里边还有循环嵌套的数据!

回来的报文很长,仔细观察最后边,发现后边一起也回来了500状况码及反常信息。

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?


长话短说,相当运用jackson,在默许状况下,关于循环目标引证,在添加了大局反常处理状况下,接口一起回来了两段相反的报文:

{
	"code":"200",
	"data":{"young":[{"name":"李四","age":23,"father":{"name":"张三","age":48}]}"
	"success":true
}
{
	"code":"500",
	"data":null,
	"msg":"Handler dispatch failed; nested exception is java.lang.StackOverflowError",
	"success":false
}

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

小朋友你是否有许多问号??

这种现象是在return后边抛出反常引起?

这就有点意思了。
形成这种现象的原因,我初步怀疑是在办法return回来往后再抛出反常导致的。

我这怀疑也不是毫无理由,详细请看我的另一篇文章 [当transcational遇上synchronized ](当transcational遇上synchronized – ()) ,里边提到过, spring运用动态代理加AOP完成业务管理。那么一个加了注解业务的办法实际上需要简化成至少3个步骤:

void begin();
@Transactional
public synchronized void test(){
    // 
}
void commit();
// void rollback();

假如在读已提交及以上的业务隔离级别下,test办法履行完毕,更新了数据但这时分还没到commit业务,但现已释放了锁,另一个业务进来读到的仍是旧数据。

类似地,这儿的test办法实际上是相同的,jackson在做序列化操作在return之前,那么会不会return回来了一次200,在return往后再抛出反常后再回来了一次500状况码?

那就运用TransactionSynchronization模仿一次在return后边的反常看回来给前端什么信息。

@Transactional
    @RequestMapping( "/clone")
    public Result test2 (){
        List<Person> list = new ArrayList<>();
        Person obj1 = new Person("张三", 48);
        Person obj2 = new Person("李四", 23);
        obj1.setFather(obj2);
        obj2.setFather(obj1);
        list.add(obj1);
        list.add(obj2);
        Person obj3 = new Person("王麻子", 17);
        list.add(obj3);
        List<Person> young = list.stream().filter(e -> e.getAge() <= 45).collect(Collectors.toList());
        List<Person> children =  list.stream().filter(e -> e.getAge()< 18).collect(Collectors.toList());
        HashMap map = new HashMap();
        map.put("young", young);
        map.put("children", children);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                if (1 == 1) {
                    throw new HttpMessageNotWritableException("test exception after return");
                }
                TransactionSynchronization.super.afterCommit();
            }
        });
        return Result.success(map);
    }

重启调用测验接口,后台打印日志

[http-nio-8657-exec-1] [com.nyp.test.config.GlobalExceptionHandler] : test exception after return

回来客户端信息:

{"code":"500","success":false,"data":null,"msg":"test exception after return"}

测验表明,并不是这个原因形成的。


到这儿,或许仔细的朋友也发现了,关于前面的猜测,关于jackson在做序列化操作在return之前,那么会不会return回来了一次200,在return往后再抛出反常后再回来了一次500状况码?其实是不合理的。
咱们在最开始接触java web开发的时分肯定是先学servlet,再学spring,springmvc,springboot这些结构,现在再回到最初的夸姣,想想servlet是怎样回来数据给客户端的?

经过HttpServletResponse获取一个输出流,不管是OutputStream仍是PrintWriter,将咱们手动序列化的json串输出到客户端。

@WebServlet(urlPatterns = "/testServlet")
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        // 经过PrintWriter 或许 OutputStream 创建一个输出流
        // OutputStream outputStream = response.getOutputStream();
        PrintWriter out = response.getWriter();
        try {
            // 模仿获取一个回来目标
            Person person = new Person("张三", 23);
            out.println("start!");
            // 手动序列化,并输出到客户端
            Gson gson = new Gson();
            out.println(Result.success(gson.toJson(person)));
            // outputStream.write();
            out.println("end");
        } finally {
            out.println("成功!");
            out.close();
        }
        super.doGet(request, response);
    }
}

我没看过springmvc这块的源码,想来也是同样的逻辑处理对吧。 在dispatchServlet里边invoke完毕目标controller获得了回来目标今后,再调用序列化结构jackson或许fastjson得到一个json目标,再经过输出流输出前端,最后一步操作或许是在servlet里也或许直接在序列化结构里边直接操作。
总归不管是在哪步,都有点不合理,假如是在序列化的时分,序列化结构直接反常了,也不应该输出200和500两段报文。


不管怎样,这儿也算是验证了@ControolerAdvice能不能捕获目标controller办法在Return今后抛出的反常,答案是能够。

现在咱们能够再来看看Fastjson在return今后进行序列化发生反常的时分,为什么不会输出200和500两段报文。


fastjson为什么没有问题


经过前文咱们知道,在同样的状况下,fastjson序列化是能够正常回来给客户端500反常的报文。

咱们现在将springmvc的序列化结构切换到fastjson。经过断点走一遍源码。观察为什么fastjson能够正常抛出反常。

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

经过调用栈信息,咱们能够很明显的观察到咱们很熟悉的distpatchServlet,再到handleReturnValue调用完结目标controller拿到回来目标,现到AbstractMessageConverterMethodProcessor.writeWithMessageConverters,终究到达GenericHttpMessageConverter.write()经过注释,哪怕是办法名和参数名,咱们也知道这儿便是开始调用详细的序列化结构重写这个办法输出回来报文到客户端了。

那么在这儿开始打个断点,这是个接口办法,它有许多完成类,这儿打断点会直接进入到详细完成类的办法。
终究来到了FastJsonHttpMessageConverter.writeInternal()

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

重点来了,如上图所示,履行到line 314行,也便是标记为1的当地就抛出反常,然后到了finally里边去了,跳过了line 337即2处真实履行write输出到客户端的操作
咱们不必去管line 314处所调用办法内部的序列化详细操作,咱们只需要知道,它在序列化预备阶段直接反常了,并没有真实履行向客户端进行write的操作。

然后反常终究被@RestControllerAdvice所捕获,输出到客户端500。


这儿能够略微再延伸一下,为什么没有履行到第2步。 由于这儿的expandCappacity办法,expand便是扩展,相当于它的write缓冲区理论上能够无限大。相比较jackson的8000缓冲长度,fastjson就能直到循环依靠犯错都还没真实向客户端输出。

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?


jackson的输出流程


现在作为对比,再回过头来看看jackson是怎样完结上述的操作的。


打到与上末节fastjson相同的断点,终究进入了jackson的序列化办法,经过右边inline watches能够看到将要被序列化的value从目标的循环引证变成了详细的若干层嵌套循环了。

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

再一路断点,来到UTF8JsonGenerator,能够观察到,jackson不是将整个回来值value一起进行序列化,而是一个目标一个field顺序进行序列化。

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

这些值将临时进入了一个buffer缓冲区,在大于outputend=8000,就flush直接输出到客户端。

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

这儿的_outputstream便是java.io.OutputStream目标。


小结

这儿能够做一个小结了。

jackson为什么会在目标循环引证的时分一起向客户端输出200和500两段报文?

勘误:由于运用objectWriter.writeValue 运用的是流输出,而缓冲区长度为8000,在循环依靠抛出反常之前,超越缓冲区长度,所以现已向前端write了200状况的数据。

fastjson为什么能正常的只输出500报文?
勘误: Fastjson运用JSONSerializer.write()同样先输出到缓冲区,但缓冲区能够扩展,理论上无限大,直到栈深度大于设置值抛出StackOverflowError之前,并没有真实向客户端输出任何报文。

@ControllerAdvice失效的场景

来自jackson的灵魂一击:@ControllerAdvice就能保证万无一失吗?

经过注释,咱们知道@ControllerAdvice默许效果于全部的controller类办法。也能够手动设置package.

@RestControllerAdvice("com.nyp.test.controller")
或许
@RestControllerAdvice(basePackages = "com.nyp.test.controller")

那么让它失效的场景便是
1.反常到不了controller层,比方在service层里经过try-catch把反常吞了。又比方到达了controller层也抛出了,但在其它AOP切面告诉里经过try-catch处理了。 2.或许不指向controller层或部份controller层,比方经过@RestControllerAdvice(basePackages = “com.nyp.test.else”)

等等。

其它只要不触碰到以上状况,正确的装备了,即使是在return后边抛出反常也能够正确处理。
详细到本文jackson的这种状况,严格意义上来讲,@ControllerAdvice也是起了效果的。只不过是jackson在序列化的过程中自身出的问题。

总结

  1. @ControllerAdvice彻底安全吗? 只要正确装备,它是彻底安全的。本文归于jackson这种特殊状况,它形成的反常状况不是@ControllerAdvice的问题。

2.形成一起回来200和500报文的原因是什么?

勘误:由于jackson运用objectWriter.writeValue 运用的是流输出,而缓冲区长度为8000,在循环依靠抛出反常之前,超越缓冲区长度,所以现已向前端write了200状况的数据。

而 Fastjson运用JSONSerializer.write()先输出到缓冲区,但缓冲区能够扩展,理论上无限大,直到栈深度大于设置值抛出StackOverflowError之前,并没有真实向客户端输出任何报文。


  1. 怎样处理这种问题?

这本质上是一个jackson循环依靠的问题。经过注解 @JsonBackReference @JsonManagedReference @JsonIgnore @JsonIdentityInfo 能够部份处理。
比方:

@JsonIdentityInfo(generator= ObjectIdGenerators.IntSequenceGenerator.class, property="name")
private Person father;

回来:

{
	"code": "200",
	"success": true,
	"data": {
		"young": [{
			"name": "李四",
			"age": 23,
			"father": {
				"name": 1,
				"name": "张三",
				"age": 48,
				"father": {
					"name": 2,
					"name": "李四",
					"age": 23,
					"father": 1
				}
			}
		}, {
			"name": "王麻子",
			"age": 17,
			"father": null
		}],
		"children": [{
			"name": "王麻子",
			"age": 17,
			"father": null
		}]
	},
	"msg": "success"
}

一起,关于目标循环引证这种状况,在代码中就应该尽量去避免。
就像spring处理依靠注入的状况,一开始运用@lazy注解处理,后边spring官方经过三层缓存来处理,再到后边springboot官方默许不支持依靠注入,假如有依靠注入默许发动就会报错。


一言以蔽之,本文说的是,关于spring mvc&spring boot运用jackson做序列化输出的时分,假如没有处理好循环依靠的问题,那么前端不能正确感知到服务器反常这个问题。

但由于循环依靠并不常见,遇到了也能有处理方案,所以看起来本文如同并没有什么卵用。

不过,没人规则必须要处理吧,当我仍是一个新手的时分,我没处理循环依靠,而一起前端又没有接纳到正确的服务端反常时,总是会有疑惑的。

而且假如引申开来的话,一切jackson在序列化半途导致的失利,都有或许发生这种状况。

从这个角度来说,算不算是jackson的一个问题呢?

不管怎样,希望本文对你能够有所启示。