建议使用以下浏览器,以获得最佳体验。 IE 9.0+以上版本 Chrome 31+ 谷歌浏览器 Firefox 30+ 火狐浏览器
请选择 进入手机版 | 继续访问电脑版
设置昵称

在此一键设置昵称,即可参与社区互动!

确定
我再想想
选择版块

Smile Agai...

发帖: 1粉丝: 0

级别 : 新手上路

Rank: 1

发消息 + 关注

发表于2019-6-25 10:55:14 468 8 楼主 显示全部楼层
[求助] 关于微服务调用契约生成和调用

环境

语言:Java

框架 : springboot

问题背景

微服务A使用spring boot开发,对接servicecomb框架之后,利用spring mvc的注解@RequestBody生成了契约,并注册到服务中心。查看服务中心注册的契约发现,在@RequestBody实体类中初始化的默认值,在服务中心都没有体现。


问题现象

微服务B使用微服务调用调动A暴露的接口,请求中省略了非必选参数,该参数带有默认值(A中RequestBody中的字段)。结果该参数经过服务调用后再A后台逻辑中变为null,未通过参数校验,即非必选参数变成了必选参数与预期结果不符。


求助

如何将默认值注册到服务中心,并通过微服务调用在没有显示设值的时候给参数附上默认值。

回复 举报
分享

分享文章到朋友圈

分享文章到微博

yhs0092

发帖: 12粉丝: 2

级别 : 注册会员

Rank: 2

发消息 + 关注

发表于2019-6-26 09:50:06 沙发 显示全部楼层

能否给一个简单的demo,展示一下问题场景啊?主要是微服务A的body请求参数上打了哪些注解,是如何设置默认值的?

点赞 回复 举报

Smile Agai...

发帖: 1粉丝: 0

级别 : 新手上路

Rank: 1

发消息 + 关注

发表于2019-7-4 18:01:07 板凳 显示全部楼层

感谢开发人员的支持,这里根据CSE文档华为云官网资料demo简单修改一下,复现问题


服务提供者声明接口如下:

@RestSchema(schemaId = "hello")
@RestController
@RequestMapping(path = "/")
public class HelloService {
   @RequestMapping(path = "hello", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
   public SimpleRet sayHello(@RequestBody Person person) {
      return new SimpleRet("Hello " + person.getName() + ", you are " + person.getAge() + " years old.");
   }

   private class SimpleRet {
      private String result;

      public SimpleRet(String result) {
         this.result = result;
      }

      public String getResult() {
         return result;
      }

      public void setResult(String result) {
         this.result = result;
      }
   }
}


Person类定义如下(age字段带有默认值):

public class Person {
    private String name;
    private Integer age = 18;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

启动项目后请求获取结果

image.png



服务消费者代码如下:

@Component
@EnableScheduling
public class ConsumerScheduler implements BootListener {
    private RestTemplate restTemplate = RestTemplateBuilder.create();
    private ObjectMapper objectMapper = new ObjectMapper();
    private boolean allowRestCall = false;

    {
        restTemplate.setMessageConverters(Lists.newArrayList(new MappingJackson2HttpMessageConverter()));
    }

    @Scheduled(fixedRate = 10_000)
    public void doConsumePer10Sec() throws IOException {
        if (! allowRestCall) return;

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<Object> requestEntity = new HttpEntity<>(objectMapper.readValue("{\"name\": \"Nick\"}", Object.class), headers);

        Object helloResult = restTemplate
                .postForObject("cse://provider-test/hello",
                               requestEntity, Object.class);

        System.out.println(objectMapper.writeValueAsString(helloResult));
    }

    @Override
    public void onBootEvent(BootEvent bootEvent) {
        if (bootEvent.getEventType().equals(EventType.AFTER_REGISTRY)) {
            allowRestCall = true;
        }
    }
}

打印结果如下,与预期不符(age字段变为null)

验证截图.PNG


问题现象:

字段设置的默认值不生效,导致业务异常。

点赞 回复 举报

yhs0092

发帖: 12粉丝: 2

级别 : 注册会员

Rank: 2

发消息 + 关注

发表于2019-7-10 15:32:52 地板 显示全部楼层

你好,严格按照你给的demo描述来执行的话,反而复现不出来你的问题。不过我推测在你的consumer端,可能不存在Person类,或者虽然存在同名的(包名都相同)Person类,但那里面你没有预定义age的值。

可以看一下下面这张截图,这个是在provider端的ServerRestArgsFilter的afterReceiveRequest方法里打断点拦截请求,可以看到provider接收到的body中确实age字段存在,且为null。建议你也在本地调试一下看看。

image.png


以下是原理说明:

由于consumer在调用provider的时候是严格按照provider的契约来做的。provider契约里会在x-java-class字段说明自己的Person参数的类名,而consumer会在加载契约后尝试在自己的classpath里找这个类,找得到的话就会沿用这个类型作为序列化Person参数的目标类型,找不到的话就会动态生成Person类型。所以如果你的consumer工程里不存在Person类或者其Person类里的age字段预定义值为null,你在发请求的时候使用的body都会先转换成这个Person对象(于是多了一个age=null字段)然后再发送到provider端。provider端会按照这个json串来反序列化Person,得到的Person参数里age自然也是null了。

点赞 回复 举报

yhs0092

发帖: 12粉丝: 2

级别 : 注册会员

Rank: 2

发消息 + 关注

发表于2019-7-10 15:57:38 5# 显示全部楼层

其实用CSEJavaSDK/ServiceComb-Java-Chassis开发服务调用方的话,我们不推荐你展示的这种调用代码开发方式。像你这样做的话,序列化、反序列化都得自己做,性能低、代码繁琐还容易出错。

建议你在consumer端的classpath里加上Person类和SimpleRet类(注意是包名、类名完全相同的,原因上文里已经解释了),这个类可以是你单独写在consumer工程代码里的,也可以是provider提供接口参数jar包,consumer引用的。

然后调用的时候consumer端发送的参数和接收的响应消息都直接使用Java对象类型,把序列化工作交给框架来处理,这样开发人员写的代码量少了,服务的运行效率还能提升。

consumer端代码例子:

@Path("/trigger")
@GET
public void doConsumePer10Sec() {
  Person person = new Person();
  person.setName("Nick");

  SimpleRet helloResult = restTemplate
      .postForObject("cse://provider/hello", person, SimpleRet.class);

  System.out.println(helloResult.getResult());
}

Person定义:

public class Person {
  private String name;
  private Integer age = 18;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Integer getAge() {
    return age;
  }

  public void setAge(Integer age) {
    this.age = age;
  }
}

SimpleRet定义:

public class SimpleRet {
  private String result;

  // 参数对象注意要添加无参的构造方法,否则在反序列化时会报错
  public SimpleRet() {
  }

  public SimpleRet(String result) {
    this.result = result;
  }

  public String getResult() {
    return result;
  }

  public void setResult(String result) {
    this.result = result;
  }
}

这样调用,发送给provider端的请求body里就能够带上默认的age=18字段了。


ps:还有两点需要注意一下

  1. 不建议将参数类型定义为内部类,所以你贴出的代码里把SimpleRet类定义在HelloService里,我们是不推荐的。

  2. 参数类型需要有无参的构造方法,否则在反序列化时会报错。例子是你给的SimpleRet,由于定义了 public SimpleRet(String result) 这样的构造方法,如果直接用在consumer端,那么consumer在接收到provider返回的body消息时,无法创建SimpleRet对象进行反序列化,需要再添加一个无参的构造方法才能保证调用成功。

点赞 回复 举报

MediaCloud...

发帖: 1粉丝: 0

级别 : 新手上路

Rank: 1

发消息 + 关注

发表于2019-7-10 16:22:05 6# 显示全部楼层

对于CSE构建的网关调用后端服务的场景,后端服务的请求类也要放到网关的classpath?后端服务不断迭代,网关不应该去感知这些变化

点赞 回复 举报

yhs0092

发帖: 12粉丝: 2

级别 : 注册会员

Rank: 2

发消息 + 关注

发表于2019-7-10 20:37:11 7# 显示全部楼层

当前版本的EdgeService网关上由于要进行一次反序列化和序列化操作,所以你们说的场景需要你们自行做一些额外的设置才能满足。

import org.apache.servicecomb.common.rest.codec.RestObjectMapper;
import org.apache.servicecomb.common.rest.codec.RestObjectMapperFactory;
import org.apache.servicecomb.foundation.common.utils.BeanUtils;
import org.apache.servicecomb.foundation.common.utils.Log4jUtils;

import com.fasterxml.jackson.annotation.JsonInclude.Include;

public class AppMain {
  public static void main(String[] args) throws Exception {
    RestObjectMapper consumerWriterMapper = new RestObjectMapper();
    consumerWriterMapper.setSerializationInclusion(Include.NON_NULL);
    RestObjectMapperFactory.setConsumerWriterMapper(consumerWriterMapper);
    Log4jUtils.init();
    BeanUtils.init();
  }
}

像这样在EdgeService启动之前设置一下序列化所使用的ObjectMapper,让它忽略掉null属性,你们的场景就能够实现了。不过这样的话属性值为null的场景就又无法表达了。

等到后面ServiceComb-Java-Chassis的弱类型内核版本发布,预计EdgeService在转发的请求的时候就不会把不存在的字段改为null值传递了。

点赞1 回复 举报

yhs0092

发帖: 12粉丝: 2

级别 : 注册会员

Rank: 2

发消息 + 关注

发表于2019-7-10 20:41:27 8# 显示全部楼层

默认情况下,对于consumer调用provider,请求body为对象类型的场景,如果使用的是rest传输方式(即HTTP协议),则在网络上传输的默认是json,“null”的含义可以表达,此时传了null字段是会覆盖provider端对象的。如果使用的是highway传输方式,在网络上传输的编码是protobuf的,由于protobuf无法表达“null”的含义,所以不会覆盖provider端参数对象字段的默认值。

最好不要破坏这个规则:传null不等于不传值。

点赞1 回复 举报

Smile Agai...

发帖: 1粉丝: 0

级别 : 新手上路

Rank: 1

发消息 + 关注

发表于7 天前 9# 显示全部楼层

感谢 @yhs0092 的细心解答, 还给我推荐了一套CSE的课程,有兴趣的小伙伴可以学习一下,讲解的很全面、细致。

点赞 回复 举报

游客

您需要登录后才可以回帖 登录 | 立即注册