上一篇,我们做到了基于浏览器访问的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 Cloud 7.2: 使用feign做服务间调用的会话保持_spring cloud

Spring Cloud 7.2: 使用feign做服务间调用的会话保持_openfeign会话保持_02

确实不一样。

怎搞?

这里不得不简单提一下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());
    }
}

这样就可以了。