前言

  最近参与的项目中,接口中返回的日期格式不对,发现项目中配置了fastjson作为spring的数据转换器,于是使用了fastjson的字段格式化转换注解 发现不起作用。这让我很疑惑,然后在fastjson的相关代码中打断点发现请求并没有进入,最后在springmvc的流程源码中发现最后调用的是jackson也就是springmvc的默认转换器,fastjson没起作用。由于在使用了@ResponseBody后才会将数据直接序列化化进响应体中,而不是渲染视图,才有可能用到fastjson转换器,所以跟了下springmvc的执行源码,最终发现了原因。

  我们在使用springmvc框架时,很多时候接口不是想解析视图,而只是想把结果写回到响应体中,例如很多时候我们只期望接口返回json或者xml格式的数据。springmvc提供了一个注解@ResponseBody。我们将其加在方法上则可以让这个方法的返回结果经过特殊转换后直接写入到响应体中。当然也可以在controller的类上直接定义@RestController 。表明整个controller的方法都是将返回结果写入到响应体中。@RestController内部其实也是就是@ResponseBody@Controller的合体

  这样我们就能返回指定格式的信息,例如在创建springboot的工程中,我们在创建controller中写入如下方法,则可以直接返回json结果

   @GetMapping(value = "/tet",produces={"application/json"} )
@ResponseBody
public UserInfo get() {
UserInfo a = new UserInfo("你好","word");
return a;
}

  或者我们将其改为xml,由于springboot默认没有引入xml转换器,所以我们需要加入一个包

      <dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
   @GetMapping(value = "/tet",produces={"application/xml"} )
@ResponseBody
public UserInfo get() {
UserInfo a = new UserInfo("你好","word");
return a;
}

  那这个注解是如何起效果的呢,我们可以根据springmvc的流程看

正文

  首先我们知道springmvc的原理是使用一个DispatchServlet来拦截servlet容器(例如jetty)所有的请求,然后根据请求的信息找到合适的处理器进行处理。我们可以看下DispatchServlet.doDispatch方法

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try {
ModelAndView mv = null;
Exception dispatchException = null; try {
//判断是不是multipart格式的数据 例如上传文件
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request); //1. 通过请求解析找到对应的HandlerExecutionChain
//里面包含了一个符合条件的Handler 和所有符合条件的HandlerInterceptor 即springmvc的拦截器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
} //2. 获取到该handler对应的适配器 即适配器模式
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 这儿如果get方法即防止重复调用
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//执行拦截器中的preHandle方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
} // 3. 根绝处理器适配器执行方法并返回视图 我们由于不解析视图 所以这儿返回为null
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//如果mv不为null 但是没有找到合适的视图 就选择一个默认的视图
applyDefaultViewName(processedRequest, mv);
//执行拦截器的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//4.将数据写入视图 并且里面会执行拦截器的afterCompletion方法
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
.......
}

    这个方法即大概说明了springmvc执行流程,中间也穿插了一些拦截器使用。我们由于是返回格式化数据,当然后面的视图数据渲染步骤也就用不上了。所以将数据写入响应体中肯定是在方法执行的步骤中就发生了。我们看下ha.handle(processedRequest, response, mappedHandler.getHandler()); 也就是方法执行的逻辑。这儿由于我们调用的是自己写的controller层的接口,所以处理器适配器的类型为RequestMappingHandlerAdapter ,我们可以看下其继承体系

  可以看下其继承了AbstractHandlerMethodAdapter  ,我们调用的handle方法会先调用抽象类中的handle方法,但这个抽象类并没有什么处理,直接交给子类处理了

    @Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { return handleInternal(request, response, (HandlerMethod) handler);
}

  所以最终我们还是来看下RequestMappingHandlerAdapter 的handleInternal方法。

@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav;
//核实下方法是否执行
checkRequest(request); // 如果有同步锁则执行这儿
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
//否则调用这儿
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
...................
return mav;
}

    这儿我们接着看普通处理 即invokeHandlerMethod方法

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response);
//这儿做了一堆的处理
...............
        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
//这儿将结果处理器设置到了ServletInvocableHandlerMethod 中
            if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
            ..............

            invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
} return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

  依旧是一大堆的处理,这儿就注意下webRequest中封装了有HttpServletResponse  后面使用webRequest写入数据会用到。还有ServletInvocableHandlerMethod 中设置了RequestMappingHandlerAdapter

中带有的HandlerMethodReturnValueHandlerComposite,即返回结果处理器,我们后面则是使用这个来处理返回结果。

  我们接着看invocableMethod.invokeAndHandle(webRequest, mavContainer);

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//获取到方法返回结果
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
//判断结果是否为nul
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
}
//判断是否有ResponseStatusReason
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
} mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
//处理返回结果
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
}
throw ex;
}
}

  终于,我们在这个方法的第一步获取到了方法执行的结果,本文不主要讲springmvc方法调用的过程,所以这儿不讲,有兴趣探究方法执行的同学可以继续往下看。这儿获取到了结果,那我们接下来要做的无非就是两件事

  1.判断这个方法的处理类型  例如解析视图,还是返回结果直接写入方法体?

  2.调用对应的数据处理器处理数据

   我们可以看到在方法的最后有对结果进行处理,我们可以看下这儿是如何进行判断的。

    @Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//通过返回值和 调用的方法信息找到对应的结果处理器
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
//结果处理器处理结果
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

  

  这儿就是两个我们上面的说的逻辑,找到对应的结果处理器。然后处理对应的结果。

我们先看下系统是如何判断并且找到我们需要的写入返回结果的处理器呢?

为方法结果选择结果处理器

  

    @Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
//是否同步
boolean isAsyncValue = isAsyncReturnValue(value, returnType); //迭代springmvc所有支持的结果处理器
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
//如果结果期望同步,但是处理器不是同步结果处理器类型的 直接跳过
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
//判断如何符合结果处理器的类型 会直接返回
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}

  这儿我们可以看到,系统是直接循环所有自带的结果处理器,将符合的返回。  这儿需要注意了,如果有多个满足条件,这儿找到第一个符合的就会直接返回,所以顺序很重。我们看下系统的结果处理器有哪些

  

  系统则是根据这个顺序来进行循环的。系统会调用每个处理器的supportsReturnType的方法来判断是否支持。我们可以先看下第一个ModelAndViewMethodReturnValueHandler的supportsReturnType方法

    @Override
public boolean supportsReturnType(MethodParameter returnType) {
return ModelAndView.class.isAssignableFrom(returnType.getParameterType());
}

  很明显我们没有返回ModelAndView,所以这个不满足,所以一直循环到第11个 也就是RequestResponseBodyMethodProcessor的时候  我们看下其判断逻辑

    @Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}

  这儿是不是看到了比较熟悉判断,当本次调用这个方法的类上有ResponseBody注解或者方法上有ResponseBody  注解则返回true。这儿就符合了我们一开始的声明逻辑了。说明这个就是我们要的结果处理器了。

  到现在结果处理器就已经获取到了:RequestResponseBodyMethodProcessor  ,那么接下来就是使用这个处理器来处理结果了,我们也可以看下处理的逻辑

结果处理器:RequestResponseBodyMethodProcessor  处理方法返回结果

  根据上面的分析,我们获取到结果处理器后,下一步执行的就是如下逻辑,我们可以看下其中的业务逻辑

handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    @Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true);
//创建server相关的请求与响应
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); //写入结果
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

  我们继续看调用的父类的writeWithMessageConverters方法。也就是核心的写入方法

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object outputValue;
Class<?> valueType;
Type declaredType; //如果返回的结果是String 特殊处理下
if (value instanceof CharSequence) {
outputValue = value.toString();
valueType = String.class;
declaredType = String.class;
}
//获取原本的信息
else {
//返回值
outputValue = value;
//返回值类型
valueType = getReturnValueType(outputValue, returnType);
//返回值声明的类型
declaredType = getGenericType(returnType);
}
//如果是流类型的 例如InutStream 特殊处理
if (isResourceType(value, returnType)) {
........
} List<MediaType> mediaTypesToUse;
//这儿查看Response中是否有手动设置ContentType
MediaType contentType = outputMessage.getHeaders().getContentType();
if (contentType != null && contentType.isConcrete()) {
mediaTypesToUse = Collections.singletonList(contentType);
}
else {
//获取请求
HttpServletRequest request = inputMessage.getServletRequest();
//获得请求接收的MediaType 一般为*/* 即接收所有
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
//获取本方法的相应产出的MediaType 如果我们没有手动设置 那么会默认支持所有的
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
mediaTypesToUse = new ArrayList<>();
//找到请求接收和我们提供的匹配的MediaType 下面则会抛异常
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
//给所有匹配的进行特殊规则排序
//例如我们的produce中设置了两个{"application/json","application/xml"}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
}
//根据一定规则挑选一个
MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
//如果选出来的不为null 就要根据MediaType 来挑选HttpMessageConverter对结果进行序列化并写入了
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
//对系统的所有消息转换器进行迭代
for (HttpMessageConverter<?> converter : this.messageConverters) { //首选要挑选出符合GenericHttpMessageConverter的子类的转换器
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
//其次要判断这个转换器要能转换当前数据 例如json转换器肯定不能转换xml数据
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(declaredType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) { //写入前在对结果做一次调整
outputValue = getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
//如果不为null 则开始写入
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
//转换器不为null 则调用转换器 将结果写入outputMessage
if (genericConverter != null) {
genericConverter.write(outputValue, declaredType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(outputValue, selectedMediaType, outputMessage);
}
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + converter + "]");
}
}
return;
}
}
} if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}

    可以看出这个方法就是核心的写入逻辑,先对结果是否是字符串做了下判断,然后对结果是否是资源类型做了下判断。然后分别找到请求接收的MediaType  和我们提供的MediaType  ,找到二者的交集,然后按特殊顺序排序,找到最优的那个MediaType  ,遍历系统的消息转换器,如果消息转换器是HttpMessage消息转换器类型,且可以转换当前消息,那么就调用消息转换器将结果写入outputMessage也就是我们的响应体中

   这儿的消息转换器即HttpMessageConverter即用来序列化数据的工具有些同学可能并不陌生,。因为现在很多项目都会将springmvc默认的json转换器 jackson换为fastjson来追求更快的序列化速度,而springmvc一般自带的如下

  注意最后圈红的需要额外引入我们一开始的包才能使用

  

  我们设置produces = {"application/json"}时很明显上面的筛选下只有MappingJackson2HttpMessageConverter符合

  设置produces = {"application/xml"}  很明显就只有MappingJackson2XmlHttpMessageConverter 符合

完结

  到此 我们就知道了@ResponseBody的作用以及spring对这个注解的处理逻辑了,主要核心步骤如下

    1.DispatchServlet的对请求进行处理,并根据请求找到合适的处理器适配器RequestMappingHandlerAdapter并调用处理方法

    2.处理器适配器根据处理器中的方法特征创建ServletInvocableHandlerMethod  方法处理对象,里面包含了有返回结果处理对象HandlerMethodReturnValueHandlerComposite

    3.方法处理对象中执行方法并获得返回值

    4.结果处理对象根据方法特征循环所有结果处理器找到满足条件的结果处理器即RequestResponseBodyMethodProcessor(如果方法上有@ResponseBody注解),调用结果处理器处理结果

    5.结果处理器中根据请求接收的MediaType和我们提供的MediaType进行匹配,并找到最合适的那个MediaType

    6.根据MediaType遍历所有的HttpMessageConverter找到能处理当前的MediaType的转换器

    7.转换器将结果写入output即响应体中

一开始的问题

  模拟下项目的环境  引入fastjson包

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

  替换转换器的教程大致如下  省略了一些其他步骤

@Configuration
public class MessageConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter f = new FastJsonHttpMessageConverter();
f.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON));
converters.add(f);
}
}

  相信看到这儿就理解了,我们在这儿添加会最后才添加到消息转换器列表中,所以根据上面的查询规则,默认的json转换器在fastjson前面,由于默认json转换器也是完全符合转换要求的,所以系统当然就直接拿到默认转换器转换了。

  通过这个图可以知道我们定义的并未有效,那么如何处理呢? 其实有两种方法,

      第一种则是采用bean定义的方式,spring会默认扫描到这个处理器加入结果处理器中,且优先级最高

  

   @Bean
public FastJsonHttpMessageConverter init(){
FastJsonHttpMessageConverter f = new FastJsonHttpMessageConverter();
f.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8));
return f; }

    我们再看处理器顺序   即得到了我们想要的fastjson序列化

    第二种则依旧采用实现WebMvcConfigurer的方式,我们在添加的时候移除掉默认的json处理器即可

然后处理器的顺序   使用了fastjson

 

@ResponseBody是如何起作用的的更多相关文章

  1. @ResponseBody 和 @RequestBody 的作用

    先提一嘴 @RequestMapping(“url”),这里的 url写的是请求路径的一部分,一般作用在 Controller的方法上,作为请求的映射地址. 代码: @RequestMapping(v ...

  2. @ResponseBody与@RestController的作用与区别

    在使用springmvc框架的时候,在处理json的时候需要用到spring框架特有的注解@ResponseBody或者@RestController注解,这两个注解都会处理返回的数据格式,使用了该类 ...

  3. @ResponseBody与@RestController

    @ResponseBody与@RestController的作用与区别 https://blog.csdn.net/xfl4629712/article/details/78528387

  4. 一个java实习生两周八次的面试经历

    以前从来没有因为求职出去面试过,一直觉得面试很可怕,没想到最近两周我也成为了面霸,两周面试八次,我的找工作之路就这样开始了!大概两个星期之前,我看着自己在招聘网站上写好的简历连投出去的勇气都没有,战战 ...

  5. SpringMVC学习--json

    简介 json数据格式在接口调用中.html页面中较常用,json格式比较简单,解析还比较方便.比如:webservice接口,传输json数据. springmvc与json交互 @RequestB ...

  6. SpringMVC框架(二)注解 (转)

    原文地址:http://www.cnblogs.com/yjq520/p/6734422.html 1.@Controller @Controller 用于标记在一个类上,使用它标记的类就是一个Spr ...

  7. SpringMVC之Ajax与Controller交互

    前面学习了拦截器,通过拦截器我们可以拦截请求,做进一步处理之后再往下进行,这里我们使用Ajax的时候会有一个问题就是会把js.css这些静态资源文件也进行了拦截,这样在jsp中就无法引入的静态资源文件 ...

  8. @Controller和@RestController源码解析

    2018年不知不觉已经走到了尾声,你还在为分不清@Controller和@Restcontroller而烦恼吗?这篇博文从源码层面分析这两个注解,值得一读. 首先贴一张源码的图,对比一下,左边是@Co ...

  9. SpringMVC相关常用注解

    @Controller: @Controller 用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller 对象 @RequestMapping: RequestMappin ...

随机推荐

  1. 【c++】零基础的自修课 01-开发工具的安装(code::blocks)

    1/开发工具的下载地址: ·(选用 visual studio开发工具,有区分mac和windows版本)https://visualstudio.microsoft.com/zh-hans/down ...

  2. linux mv命令 cp命令

    mv mv [options] source dest -f : 在mv操作要覆盖某已有的目标文件时不给任何指示 命令格式 运行结果 mv 文件名 文件名 将源文件名改为目标文件名 mv 文件名 目录 ...

  3. 在一个C程序中,main()函数可以放在哪?

    C语言规定,在一个C程序中,main()函数的位置(). A.必须在系统调用的库函数之后 B.必须在程序的开始 C.必须在程序的最后 D.可以在任意位置 答案:D [解析] 每个C程序有且只有一个主函 ...

  4. C语言运算符优先级和结合性一览表

    所谓优先级就是当一个表达式中有多个运算符时,先计算谁,后计算谁.这个其实我们在小学学算术的时候就学过,如1+4÷2. 但是C语言中的运算符已经远不止四则运算中的加减乘除了,还有其他很多运算符.当它们出 ...

  5. ILM --interface logic model

    1.描述接口逻辑的模型. 2.包括 netlist  spef sdc def 3.所有以上文件只描述和接口相关的逻辑,其他逻辑一概排除 3.用于STA/PR/DC的hierachical flow, ...

  6. INCA二次开发-INCACOM

    1.INCA介绍 INCA是常用的汽车ECU测试和标定的,广泛应用于动力总成等领域.INCA提供了丰富的接口,供用户自动化.定制化.本公众号通过几篇文章,介绍下一些二次开发的方法,本篇介绍INCA-C ...

  7. wix在使用heat自动生成wxs时添加windows服务组件

    最近需要给安装包增加一个windows服务组件,按照我的理解,我以为只需要Product.wxs加一段如下的标签就可以了 <Componet Id="myservice"&g ...

  8. C语言如何判断单个数字是否溢出:

    如何判断一个输入或者输出转化的单个数字是否溢出: if( num>0x7fffffff || num<(signed int)0x80000000) 注意: int类型的最大正数:0x7f ...

  9. 结构体数组排序:1004 成绩排名 【pta】

    结构体模板 struct STU { string name; //用string可以代替char string num; int s; }; sort是用快速排序实现的,属于不稳定排序,stable ...

  10. AttributeError: This QueryDict instance is immutable

    当写添加注册后端时,运行当运行时,会出现: "AttributeError: This QueryDict instance is immutable": 因为默认的 QueryD ...