上一篇,我们做到了基于浏览器访问的session一致。
但,我们服务间的调用依然无法保持session,证据如下:
我们分别调整一下service1和service2中IndexDemoController的代码,让其返回sessionID:
package com.hao1st.service1.biz.indexdemo.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexDemoController {
/**
* 浏览器访问
* @return
*/
@GetMapping("/index")
public String index(){
return "您访问的是service1的index";
}
/**
* rpc访问
* @return
*/
@PostMapping("/pindex")
public String postIndex(HttpServletRequest request) {
return "您访问的是service1的postIndex,会话ID是" + request.getSession().getId();
}
}
service2的也是大同小异,这里就不占用篇幅了。
然后我们启动eureka、gateway、service1、service2,分别访问之前写好的两个服务的服务间调用方法localhost:9001/SERVICE1/rpc/invoke和localhost:9001/SERVICE2/rpc/invoke:
确实不一样。
怎搞?
这里不得不简单提一下spring会话保持的工作原理,是通过将session信息保存在请求头中进行传递,并存储在后端,比如redis,这样所有服务可以通过查找redis的方式来判断当前会话是否一致。
我们直接访问服务时,spring session redis会帮我们自动处理这些事情,但使用feign进行服务间调用的时候,因为没有携带请求头,所以这部分信息无法存储在redis中。
so,解决思路也很简单,就是把请求头带上。
拿service1 rpc调用service2举例,我们只需要在service1中将controller的对应方法加上参数
@RequestHeader HttpHeaders httpHeaders
就可以拿到前端的请求头,像这样
@GetMapping("/invoke")
public String rpcService2(@RequestHeader HttpHeaders httpHeaders) {
...
}
然后,我们在进行rpc调用的时候,将httpHeaders作为参数传过去,下面代码做一下对比,第一行是原来的,第二行是传递httpHeaders的
return cbFactory.create("rpcService2").run(() -> service2Rpc.rpcService2(), throwable -> "service2的服务不可用");
return cbFactory.create("rpcService2").run(() -> service2Rpc.rpcService2(httpHeaders), throwable -> "service2的服务不可用");
完整的代码如下
package com.hao1st.service1.biz.rpcdemo.controller;
import com.hao1st.service1.biz.rpcdemo.rpc.Service2Rpc;
import jakarta.annotation.Resource;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/rpc")
public class RpcController {
@Resource
private Service2Rpc service2Rpc;
/**
* 熔断工厂类
*/
@Resource
private CircuitBreakerFactory cbFactory;
@GetMapping("/invoke")
public String rpcService2(@RequestHeader HttpHeaders httpHeaders) {
// 调用接口访问service2
return cbFactory.create("rpcService2").run(() -> service2Rpc.rpcService2(httpHeaders), throwable -> "service2的服务不可用");
}
}
对应的,rpc调用接口中也要加上参数
package com.hao1st.service1.rpc;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
// 指定要调用的模块,value即配置文件中的spring.application.name
@FeignClient(value="service2")
public interface Service2Rpc {
// api路径
@PostMapping("/pindex")
String rpcService2(@RequestHeader HttpHeaders httpHeaders);
}
注意
如果首次访问服务就需要rpc调用其他服务的话,尽量避免。因为首次访问就是rpc的话并不能保证session一致,需要先直接访问服务,然后再访问需要进行rpc调用的功能。
如果实在需要的话,我们可以先获取一下session,这样在第二次访问需要进行rpc调用的服务时,也可以保证session一致。
具体的做法,其实在真正的项目中一定会在拦截器中对HttpServletRequest进行操作,也就不存在这个问题了。所以我们的Demo可以简单写一个拦截器
package com.hao1st.service2.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 默认拦截器
*/
public class DefaultInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 临时代码,操作一下request,有助于session的保存
request.getSession();
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
然后将拦截器添加到配置中
package com.hao1st.service2.conf;
import com.hao1st.service2.interceptor.DefaultInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
// 添加自定义拦截器
registry.addInterceptor(new DefaultInterceptor());
}
}
这样就可以了。