优雅实现跨域通信,对代码侵入性小
实现jsonp跨域通信
实现基于jsonp的跨域通信方案
原理
浏览器对非同源ajax请求有限制,不允许发送跨域请求
目前跨域解决方案有两种
cros为新规范,通过一个head请求询问服务器是否允许跨域,若不允许则被拦截
jsonp则为利用浏览器不限制js脚本的同源性,通过动态创建script请求,服务器传递回一个js函数调用语法,浏览器端按照js函数正常调用回调函数
实现思路
首先确定服务器端应该如何返回数据
一次正确的jsonp请求,服务器端应该返回如下格式数据
1 2 3
| jQuery39948237({key:3})
|
其中,jQuery39948237为浏览器端要执行的函数名,该函数由ajax库动态创建,并将函数名作为一个请求参数和该次请求的其余参数一并发送,服务器端无需对此参数做过多处理
{key:3}为此次请求返回的数据,作为函数参数传递
其次,服务器端如何处理?
为了兼容jsonp和cros方案,服务器端应该在请求带有函数名参数时返回函数调用,否则正常返回json数据即可
最后,为了减少代码的侵入,不应该将上述流程放入一个Controller正常逻辑中,应该考虑使用aop实现
实现
前端
前端本次使用jquery库 (本来想用axios库的,但是axios不支持jsonp)
代码如下
1 2 3 4 5 6 7 8 9
| $.ajax({ url:'http://localhost:8999/boot/dto', dataType:"jsonp", success:(response)=>{ this.messages.push(response); } })
|
Jquery默认jsonp函数名参数name为callback
后端
本次采用aop实现
具体思路为: 给Controller添加后切点,判断request是否有函数名参数,如果有则修改返回的数据,没有则不做处理
而aop又有两种方案
- 常规aop,自己定义切点
ResponseBodyAdvice,Spring提供的可直接用于数据返回的工具类
本次使用第二种方案
首先是Controller的接口实现
1 2 3 4
| @RequestMapping("dto") public Position dto() { return new Position(239, 43); }
|
返回一个复杂类型,Spring会自动对其做json序列化操作
然后的ResponseBodyAdvice实现
该类全路径为:org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
|
@RestControllerAdvice(basePackageClasses = IndexController.class) public class JsonpAdvice implements ResponseBodyAdvice {
private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper mapper;
private final String callBackKey = "callback";
@Override public boolean supports(MethodParameter methodParameter, Class aClass) { logger.debug("返回的class={}", aClass); return true; }
@Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (body == null) return null; logger.debug("body={},request={},response={},media={}", body, serverHttpRequest, serverHttpResponse, mediaType.getSubtype());
if (serverHttpRequest instanceof ServletServerHttpRequest) { HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
String callback = request.getParameter(callBackKey);
if (!StringUtils.isEmpty(callback)) { if (body instanceof String) { return callback + "(\"" + body + "\")"; } else { try { String res = mapper.writeValueAsString(body); logger.debug("转化后的返回值={},{}", res, callback + "(" + res + ")");
return callback + "(" + res + ")"; } catch (JsonProcessingException e) { logger.warn("【jsonp支持】数据body序列化失败", e); return body; } } } } else { logger.warn("【jsonp支持】不支持的request class ={}", serverHttpRequest.getClass()); } return body; } }
|
使用@RestControllerAdvice指明切点
bug
经过此步骤,理论上即可实现jsonp调用了。
然而实际测试发现,由于Spring json序列化策略的问题,如果返回jsonp字符串,json序列化之后,将会添上一对引号,如下
应该返回
1
| Jquery332({"x":239,"y":43})
|
实际返回
1 2
| "Jquery332({\"x\":239,\"y\":43})"
|
导致浏览器端无法正常运行函数
经多方查找资料后得知
由于在ResponseBodyAdvice中修改了实际的返回值类型为String,而字符串类型经过Jackson序列化后就会加上引号
解决办法为:修改默认的json序列化MessageConverter处理逻辑,对于实际是String的不做处理
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @Component public class Jackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { if (object instanceof String) { Charset charset = this.getDefaultCharset(); StreamUtils.copy((String) object, charset, outputMessage.getBody()); } else { super.writeInternal(object, type, outputMessage); } } }
@Configuration public class MvcConfig implements WebMvcConfigurer {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired private MappingJackson2HttpMessageConverter converter;
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converter.setSupportedMediaTypes(new LinkedList<MediaType>() {{ add(MediaType.TEXT_HTML); add(MediaType.APPLICATION_JSON_UTF8); }}); converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); converters.add(converter); } }
|
2021-07-23 补充
再次使用该方案实现某功能时,由于我忘了已经解决过双引号问题,于是我又想办法解决了一次,这次的方案更加简单,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Bean public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() { ObjectMapper objectMapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addSerializer(Long.class, new JsonSerializer<Long>() { @Override public void serialize(Long anEnum, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(String.valueOf(anEnum)); } });
module.addSerializer(String.class, new JsonSerializer<String>() { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (gen.getOutputContext().getParent() == null) { gen.writeRaw(value); } else gen.writeString(value);
} }); objectMapper.registerModule(module);
return new MappingJackson2HttpMessageConverter(objectMapper); }
|
这次是对序列化工具ObjectMapper下手,该方案更加友好,而且ObjectMapper还可以用在别的地方,不单局限在类型转换上
代码
具体实现可查阅github