深入SpringMVC聊跨域问题的最佳实践
阿里妹导读
一、写在最前面,跨域和Jsonp
浏览器为了防止恶意网站进行跨站点的请求伪造,会限制不同站点之间的资源交互,这种行为被称为浏览器的同源策略(Same Origin Policy)。简单来说,在站点A的页面中的请求,请求url中的域名一般不能是其他站点。在不同站点之间进行资源访问,称为跨域(即Cross-Origin)。
当产生跨域请求时,即使响应结果从服务端返回了,浏览器也会将其拦截,产生CORS异常,提示:“Access has been blocked by CORS policy”。
然而,在多个安全的站点之间,互相访问数据是非常普遍的,比如在商家工作台页面(域名是i.alibaba.com)会访问其他子系统(比如数参data.alibaba.com)的接口,因此,需要为跨域问题提供解决方案。解决跨域问题的方法有两种:CORS和Jsonp。
CORS(Cross-Origin Resouce Sharing)
CORS是W3C官方提出的跨域解决方案。当在站点A的页面访问站点B时,若站点B的服务在响应中添加以下header,则浏览器不拦截对应的结果。header包括:
Access-Control-Allow-Origin | 允许的站点(ip或域名),也就是原站点A |
Access-Control-Allow-Credentials | 是否可以携带Cookie |
Access-Control-Allow-Methods | 允许的HTTP方法,GET/POST等 |
Access-Control-Allow-Headers | 允许的Header |
JSONP(JSON with Padding)
Jsonp并不是官方提供的跨域解决方案,但是因为用的早,现在仍有非常多的历史接口还是基于jsonp。它主要的思路是:<script>标签中的脚本内容不受浏览器的同源策略限制,所以可以将资源数据“伪装”成js脚本。
对于服务端来说,用jsonp来处理跨域,需要做两件事:
1. 填充json
1.首先,当前端发起jsonp请求时,会动态插入一个<script>脚本用于请求数据,并提供一个回调函数,函数名作为查询参数callback,函数体是得到数据后的回调逻辑。如下图为jsonp_1718436528810_81650。
2.其次,服务端填充json。服务端的原始结果为json格式,将其填充后,会得到一条函数调用语句。函数名为上一步的callback入参,函数实参是原始的json结果。这个padding的过程也是jsonp名称的由来。
3.最后,前端执行填充后的结果,于是在回调函数jsonp_1718436528810_81650中可以获取原始json数据。
2. 设置响应的Content-Type
为了绕过同源策略,必须让浏览器认为这次请求返回的内容是一个script脚本,因此需要让响应的Content-Type是“application/javascript”。
如果返回的内容是jsonp,但Content-Type是“application/json”,浏览器会无法识别,并产生ORB(Opaque Response Blocking)异常,提示:“No data found for resource with given identifier”。
对比两种跨域的解决方案,CORS清晰简单,正在逐步替代Jsonp。很多框架和三方库(如spring mvc,fastjson等)也在逐步废弃jsonp的相关实现。但是,后者的使用非常广泛,基于现实情况,很多系统里是两种方式并存的。
二、问题背景
2.1 问题表现
在对某个Web系统做技术升级的过程中,有Jsonp接口出现跨域问题。现象是:
1.CORS相关的http header缺失(即“Access-Control-Allow-Credentials”等)。虽然在这个场景并不必要,但这些header被设置过,却未生效。
2.Jsonp接口返回时,http header中的Content-Type值为“application/json”,而不是“application/javascript”,导致出现ORB错误。
2.2 原实现方案
出问题的接口为jsonp接口,它通过自定义Spring MVC的拦截器(Interceptor)和MessageConvertor实现json padding并设置content-type,原实现方案如下。
1.声明自定义拦截器JsonpInterceptor,若请求URI以jsonp结尾,则需经过该拦截器
public class MvcConfiguration implements WebMvcConfigurer {
private JsonpInterceptor jsonpInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jsonpInterceptor)
.addPathPatterns("/**/*.jsonp");
}
}
2.声明类JsonpWrapper。
3.在controller中,对所有的返回值做统一处理:若是jsonp请求,则将结果对象包裹在JsonpWrapper中。
public static Object getJsonOrJsonpObj(String callback, Object obj) {
if (StringUtils.isBlank(callback)) {
return obj;
} else {
JsonpWrapper jsonp = new JsonpWrapper();
// callback是一个入参,且会返回到浏览器,因此需要做处理,防止脚本注入
jsonp.setCallback(SecurityUtil.escapeHtml(callback));
jsonp.setValue(obj);
return jsonp;
}
}
4.自定义JavascriptConvertor,并替换SpringMVC默认的json convertor。Message Convertor是Spring MVC用于处理请求和返回数据的类,更多的细节先按下不表。
5.当返回结果为JsonpWrapper时,按照jsonp的格式,在结果前后填充数据,输出最终结果。AbstractJackson2HttpMessageConverter是SprigMVC提供用于处理json类数据的Message Convertor的父类。
public class JavaScriptMessageConverter extends AbstractJackson2HttpMessageConverter {
// 省略其他
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
String callback = (object instanceof JsonpWrapper ? ((JsonpWrapper) object).getCallback() : null);
if (callback != null) {
generator.writeRaw("/**/");
generator.writeRaw(callback + "(");
}
}
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
String callback = (object instanceof JsonpWrapper ? ((JsonpWrapper) object).getCallback() : null);
if (callback != null) {
generator.writeRaw(");");
}
}
}
6.在拦截的后置处理中,向response中写入CORS请求,并修改ContentType为application/javascript
public class JsonpInterceptor extends HandlerInterceptorAdapter {
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) {
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS");
// 省略safeOrigin获取过程
response.setHeader("Access-Control-Allow-Origin", safeOrigin);
response.setHeader("Content-Type", "application/javascript");
}
}
上面提到的JsonpInterceptor,JsonpWrapper,JavascriptConvertor均为自定义实现。这个方案在很长的时间里也能正常工作。但请注意,设置Http Header的位置是在拦截器的afterCompletion方法中。
2.3 问题定义
将问题表现和原实现方案结合看,流程中,最后一步的setHeader和问题直接相关。所以从这里开始,只需要解答一个问题,那就是:
为什么自定义的interceptor中,对response的header设置不生效了?
三、Spring MVC的工作流程
拦截器(Interceptor)是Spring MVC提供的工具。因此下面简要回顾Spring MVC的工作流程,且重点放在Response的Header和内容写入上。
3.1 一张古老的时序图
上图展示了Spring MVC的几个关键组件:
7.Interceptor:在调用Controller前后拦截,可以添加处理逻辑,用于日志,鉴权等。拦截器是可选的。
上图同时也描述了Spring MVC工作中的一个标准流程,可以简要概括为:
5.View被调用,页面被解析并返回给浏览器;
3.2 规范越多,责任越大
上图的时序图比较简洁,从中能鸟瞰Spring MVC的工作流程。但是它不能反映框架里所有的工作场景,且跟本篇讨论的流程也不完全匹配。因为请求可以返回视图页面,也可以返回对象,但无论json还是jsonp对象都不是视图页面,所以在这里View相关的逻辑并不会被运行。
究其原因,是HTTP的规范内容众多,为此,Spring MVC需要支持各种类型的协议,以处理对应的逻辑。正如返回值可以是页面View,也可以是json对象,甚至在SSE协议中,还可以是HttpEmitter对应的长连接。而上一节中,被刻意忽略的组件HandlerAdapter,正是Spring MVC处理这一工作的核心角色,因此责任重大。
面向协议的抽象,HandlerAdapter
HandlerAdapter是处理业务的核心角色,业务逻辑的处理器——controller是被该类的实例代理执行,为了处理不同的HTTP场景,HandlerAdapter(以下简称HA)除了调用最终的controller外,还负责参数解析,返回值处理。因此,在HA中维护了所有场景的参数解析,返回值处理,甚至ControllerAdvice切面,而不同场景的差异,则在HA中被选择并处理。
RequestMappingHandlerAdapter是默认的HandlerAdapter实现。
上图将重点放在返回值的处理和解析上,流程可概括为:
5.该ValueHandler调用handleReturnValue,处理这个返回值。
从第3步开始,就进入到返回值的解析和处理环节。以下将详细说明3-5步具体干了什么。
处理返回值的组合模式
在HandlerAdapter中维护了所有返回值的处理逻辑,这些逻辑都实现了HandlerMethodReturnValueHandler,而HA要统一管理这些Handlers,使用了一种称为Composite的设计模式(组合模式)。
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();
HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
// ...
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
// ...
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
// ...
}
void handleReturnValue
}
简单来说,存在一个Composite对象,维护了所有ReturnValueHandler的列表,其中最重要的方法有两个:
1.selectHandler根据controller的返回值和方法签名,选择一个ReturnValueHandler。每个handler需要自己实现一个名为supportsReturnType的方法,来说明自己能否解析某种controller的返回值。
2.handleReturnValue处理返回值。先选择一个ReturnValueHandler,再调用该handler的解析方法。
用方法签名选择处理器
不同的handler,根据要求实现各自的supportsReturnType,就可以解决不同HTTP场景下的返回值处理。
本文的问题场景:JSON和JSONP格式的返回值,均被RequestResponseBodyMethodProcessor所处理。原因就在于他的supportsReturnType方法的实现:若controller的返回值被ResponseBody注解所修饰,则可被该类处理。
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// ...
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
最终,RequestResponseBodyMethodProcessor的方法handleReturnValue被用来处理json/jsonp的返回值。
显然,“如何区分并处理json/jsonp的返回值”是问题的关键所在(将在5.2节详细描述),但在陷入细节之前,我们跳出来看一下跟Spring MVC相关的另外一个问题,那就是拦截器。
执行的责任链,Interceptor和HandlerExecutionChain
在那张古老的时序图里,除了HandleAdapter外,还有一个被刻意忽视的角色——Interceptor。
当DispatcherServlet查询HandlerMapping时,返回的对象并不是Controller,而是HandlerExecutionChain。该对象根据请求的url构造,维护了该请求需要运行的Spring MVC拦截器。
HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
// ...
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
// 通过匹配该interceptor和请求的url,来判断是否应该加入到执行链中
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
// ...
}
在DispatcherServlet的执行过程中,会依次使用chain去调用对应的interceptor。Spring提供的拦截器有三个可实现的方法,分别对应执行过程的三个阶段:
preHandle | controller调用前 |
postHandle | controller调用后,视图渲染前 |
afterCompletion | 视图完成渲染后 |
因此,3.1中对“Controller执行完业务逻辑,返回视图ModelAndView”这段描述应该被细化成:
4.视图返回后(也可能根本不存在视图),调用interceptor中的afterCompletion;
由此可见:HandlerAdapter和HandlerExecutionChain(或者说Interceptor)是两个平级的角色,它们各司其职,需要被独立看待。
postHandle和afterCompletion的区别
从时序图来看,拦截器里的postHandle和afterCompletion方法之间有诸多差异,通常认为:
postHandle:在控制器方法(Controller的处理方法)执行完毕并且视图对象已经确定(但还未进行视图渲染)之后被调用。这意味着你在这个阶段仍然有机会修改模型数据(Model)或者视图(View)。
afterCompletion:在完整的请求处理完毕之后被调用,包括视图渲染完成和响应数据已经发送给客户端之后。这意味着所有响应处理都已经完成,包括视图渲染和流的关闭。
而原方案中,设置CORS和ContentType header的位置均在afterCompletion方法中,那么问题的答案好像呼之欲出:响应的处理已经完成,再设置任何Http Header都无法生效了。
四、被误解的拦截器
再次回忆那张古老的时序图,里面有个根本不会在本文场景里出现的重要角色——视图(View)。但是,每当提到拦截器中两个方法(postHandle和afterCompletion)的区别时,均是在视图的场景下进行探讨。颇有“用前朝的剑斩本朝的官”的意思。
那么,afterCompletion方法被调用时,响应是否已经完成,不可以再设置header了呢?根据实际的观察,不一定。也就是说,有可能已完成,也有可能未完成。这涉及到一个概念叫做 response的committed。
4.1 Response的Committed
调用response的write方法,只会将内容写到缓冲区。如果一个响应被提交(committed),那响应的内容才从缓冲区发送到客户端(比如浏览器)上。
如果响应被committed,那此时再设置响应的header是无效的。
以下是tomcat-embed-core-9.0.31版本的Response实现(org.apache.catalina.connector.Response),当isCommitted为true, setHeader方法会立即返回:
public void setHeader(String name, String value) {
//...
if (isCommitted()) {
return;
}
//...
char cc=name.charAt(0);
if (cc=='C' || cc=='c') {
if (checkSpecialHeader(name, value)) {
return;
}
}
getCoyoteResponse().setHeader(name, value);
}
本文所提及的部分jsonp接口中,由于在HandleAdapter的处理范围里,并没有组件主动commit响应,因此无论在拦截器里的postHandle还是afterCompletion方法里,响应都未committed,此时setHeader都可以生效。在整个请求流程的末尾(超出了Spring MVC的作用范围),才由tomcat处理缓冲区,将响应发送到浏览器。
而这就是为什么原实现方案的setHeader写在afterCompletion里,但接口却一直能正常工作的原因。
4.2 preHandle才是setHeader的合理位置
但既然发生了问题,说明某些jsonp接口并不能如上所述地正常setHeader。
通常Request和Response都会被框架层层包装,下面的逻辑导致Response的commit操作是无法被预期的。
public abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
public void write(char[] buf, int off, int len) {
checkContentLength(len);
this.delegate.write(buf, off, len);
}
private void checkContentLength(long contentLengthToWrite) {
this.contentWritten += contentLengthToWrite;
boolean isBodyFullyWritten = this.contentLength > 0
&& this.contentWritten >= this.contentLength;
int bufferSize = getBufferSize();
boolean requiresFlush = bufferSize > 0 && this.contentWritten >= bufferSize;
if (isBodyFullyWritten || requiresFlush) {
doOnResponseCommitted();
}
}
}
上述代码,反映了HandlerAdapter将controller的返回值写入response的逻辑片段:
3.该类在写数据前,会checkContentLength,一旦超过缓冲区,则触发Response的commit(第16行)。
因此引入了该Response的实现,或者缓冲区被调整,就会导致某些接口在HandlerAdapter的处理过程中被commit。
而由于HandlerAdapter处理返回值的过程在postHandle和afterCompletion被调用之前,因此,此时在这两个方法中setHeader均不会生效。
出问题的jsonp接口的作用是获取全量美杜莎文案,返回数据量较大,恰好对应了缓冲区满的可能。而到底是因为技术升级后,新引入了OnCommittedResponseWrapper还是因为缓冲区被隐性调整,已经无从考证了。
综上,因为无论在postHandle和afterCompletion中setHeader都可能不生效,所以setHeader的合理位置是interceptor的preHandle方法内。
作出上述调整后,CORS相关的Header被成功设置,但是Content-Type则始终还是application/json,依旧不符合预期。
五、特立独行的响应类型
5.1 Content-Type,不可思议的脱节
首先,陈述一个事实,HandleAdapter中的HandlerMethodReturnValueHandler会将header写入Response。
其次,提出一个假设,因为Interceptor的preHandle在该环节之前,那么前一个步骤设置了content type为“application/javascript”,在后续的处理中应该以设置的为准。
由于最终设置的,并不是上述假设的结果,所以需要确认header的读取和写入两个步骤是如何发生的。
1.读取阶段,HandlerMethodReturnValueHandler是如何获取content-type的
从处理返回值的父类AbstractMessageConverterMethodProcessor可见,HandleAdapter获取ContentType,是从传入的Response的header中拿到的,见下方第4行。
void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
//...
MediaType contentType = outputMessage.getHeaders().getContentType();
//...
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
// ...
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
//...
return;
}
}
getHeaders仅仅是对Response的代理,由此可知,contentType确实从传入的Response的header里获取。
public MediaType getContentType() {
// getFirst是从Header中获取该name的第一个Header
String value = getFirst(CONTENT_TYPE);
return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null);
}
但是这里却出现了一个难以理解的现象:在Interceptor中被设置过的Content-Type无法在此处被获取到。
2.写入阶段,content-type是如何被设置的
显然,content-type是在拦截器的preHandle方法中,通过调用Response的setHeader设置的。但是,深入到具体实现,也就是tomcat response中设置content-type的位置能发现:若header是content-type,是无法通过setHeader被设置的(第13-15行)。
public void setHeader(String name, String value) {
//...
char cc=name.charAt(0);
if (cc=='C' || cc=='c') {
if (checkSpecialHeader(name, value)) {
return;
}
}
//...
}
private boolean checkSpecialHeader(String name, String value) {
if (name.equalsIgnoreCase("Content-Type")) {
setContentType(value);
return true;
}
return false;
}
也就是说,:tomcat设置content-type header的实现和spring mvc获取content type的实现产生了脱节。
1.在tomcat的checkSpecialHeader方法中,若是Content-Type,则不会设置header。
2.下游的spring mvc则又是从header中获取Content-Type。
这种不可思议的脱节,导致在拦截器的preHandle方法中,无论如何设置Content-Type,在HA处理结果时,都无法获取人为设置的结果。
这里引申出两个问题:
1.在拦截器的postHandle中设置是否可以呢?
在上一节已经说过,是否可以在postHandle中设置header是无法预期的。所以可能有些接口能设置成功,有些结果返回内容较多(达到缓冲区上限),则无法设置成功。
2.既然无法主动设置response的Content-Type,那所有的Http请求岂不是都有问题?
答案也是显然的,那就是“没有问题”。
因为response的content-type不应该由人为决定,而是Spring MVC的自主选择。
5.2 MessageConvertor,Spring MVC的自主选择
在3.2节中遗留了一个关键问题:“如何区分并处理json/jsonp的返回值”。同时5.1节也得到结论,response的Content-Type应该由Spring MVC自主选择。而Message Convertor则是实现这一选择的关键角色。
待候选的Message Convertor
由于在controller的方法签名中,返回值被ResponseBody所修饰,所以HandlerAdapter(更具体的,就是RequestMappingHandlerAdapter)将结果的处理逻辑交给了RequestResponseBodyMethodProcessor。
所以“区分并处理json/jsonp的返回值”的控制权就交给了它。从下图看具体的处理逻辑:
首先,涉及的几个角色有:
5.UTF8JsonGenerator:使用到的输出工具类,convertor会使用该工具类和Response交互,将结果写入。
其次,解析controller返回对象并写入Response的流程为:
1.获取Acceptable的内容类型。解析request中的Accept字段,查看浏览器可以接收的类型。如果是“*/*”表示所有类型都可以接收。
2.获取Producible的内容类型。遍历系统中的所有Message Convertor,看针对该响应能产生的所有类型。具体是:依次调用Convertor的canWrite方法,判断该Convertor是否能处理该响应。若能处理则返回改Convertor能支持的所有类型。
// clazz为返回对象的类型
boolean canWrite(Class<?> clazz, MediaType mediaType)
3.判断Producible的类型是否可被accept,仅保留可接收的类型
4.对可处理的类型进行排序。
5.遍历Message Convertor,按照顺序进行输出。
总结一下就是:controller返回的对象所对应的content-type,以及输出的方式,取决于哪一个Message Convertor能够处理该对象。
比较理想的情况是,对于每一种类型,Spring MVC中仅注册了一个对其处理的Message Convertor。但是在本文描述的原实现方案中,情况变得复杂。特别是上面的第4-5步,这所谓的排序,开始变得不清不楚了。
MediaType顺序很重要
在最后的一节中,我们先给出结论:MediaType的顺序是有错误的,未被正确设置。
在原方案中,自定义的Convertor所支持的MediaType为:json和javascript,导致HA输出的content-type始终为json,但是由于拦截器在afterCompletion方法中会再重新设置content-type为javascript,所以jsonp的请求一直可以正常返回。
如今设置content-type的位置被改到preHandle中,MediaType的错误顺序问题被暴露了出来。
排序的方法为MediaType.sortBySpecificityAndQuality中,逻辑是:
4.subType不同的MediaType,互不影响。type为参数中/的后半部分,比如application/json和application/javascript的subType分别是json和application。它们的顺序也根据系统定义。
根据上述原则,因为自定义的Convertor所支持的MediaType为:json和javascript,它们的subType不同,q-value也都是1,因此顺序就是代码里定义的顺序。
经过自定义的convetor处理的响应,最终输出的content-type始终是json,也就是第一个。
六、问题解决
基于上述的分析,为了解决2.1中的问题,最终的方案为:
1.对CORS的setHeader,位置被放置在拦截器的preHandle方法中
2.自定义的Message Convertor只支持MediaType为application/javascript的类型,通过改写canWrite方法,过滤非jsonp的请求。
boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 当返回值为JsonpWrapperObject类型时,才是jsonp的请求
// 其他的请求,交由spring mvc默认对json的Message Convertor处理
return clazz == JsonpWrapperObject.class;
}
3.调整Message Convertor在Spring MVC中的注册顺序,自定义对jsonp处理的Convertor排第一个。
七、跨域问题的最佳实践
综上会发现,为了实现Jsonp,这里做了非常多tricky的操作(拦截器+自定义的convertor)。看起来,这并不是一个最佳的实践方案。
是的,对比跨域的实现方式来说,使用CORS相比jsonp要简单太多。
同时就算是jsonp,一些开源代码也提供了更好的支持。比如:fastjson提供的JSONPResponseBodyAdvice,实现全局的controller切面。在5.2节的时序图中,也可以看到AbstractMessageConverterMethodProcessor中有一个步骤是通过Advice来在响应体写入前做一些处理。
JSONPResponseBodyAdvice的实现如下:
@ControllerAdvice
public class JSONPResponseBodyAdvice implements ResponseBodyAdvice<Object> {
//...
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return FastJsonHttpMessageConverter.class.isAssignableFrom(converterType)
&&
(returnType.getContainingClass().isAnnotationPresent(ResponseJSONP.class) || returnType.hasMethodAnnotation(ResponseJSONP.class));
}
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
ResponseJSONP responseJsonp = returnType.getMethodAnnotation(ResponseJSONP.class);
// ...
}
// ...
}
但是这就要求controller的返回值,需要被fastjson的ResponseJSONP注解所修饰。
出于开发和回归成本的考虑,本文并没有使用这种方式,然是继续沿用原有的拦截器和Message Convertor。
希望以后再用到jsonp的时候,能够使用更简单的实现方案。哦,不对,下次解决跨域问题时,应该不会再用jsonp了。
高可用及共享存储 Web 服务
随着业务规模的增长,数据请求和并发访问量增大、静态文件高频变更,企业需要搭建一个高可用和共享存储的网站架构,以确保网站服务能够7*24小时运行的同时,可保障数据一致性和共享性,并降低数据重复存储的成本。快点击阅读原文体验吧~
微信扫码关注该文公众号作者