用户上下文打通与Feign的调用

在前几篇中已经将微信,网关与鉴权微服务全部打通,这次我们进行用户上下文打通,与微服务之间的调用。

用户上下文打通:

首先先思考一下,当我们成功登录的时候,网关会获取到当前用户相关的信息,比如说用户名等等,现在我们需要在catalogue服务中获取到当前用户的信息,我们应该怎么做呢,以前的思路就是,在catalogue服务中再进行一次获取不就好了。理论上可以,但是当微服务多了,难道每个微服务都需要进行获取吗,那么网关的意义也就不大了。

所以我们的目的是在网关层,将拦截的信息处理好,分发给每个微服务,做统一的处理,那么就引出了我们今天实现的第一个目的,打通用户的上下文。

新增包与实现类:

我们在gateway服务中新增filter包,新增一个类为LoginFilter

 LoginFilter:

代码如下:

package com.yizhiliulianta.gateway.filter;

import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.nacos.api.utils.StringUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 登录拦截器
 */
@Component
@Slf4j
public class LoginFilter implements GlobalFilter {
    @Override
    @SneakyThrows
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();

        String url = request.getURI().getPath();
        log.info("LoginFilter.filter.url:{}", url);

        if (url.equals("/user/doLogin")) {
            return chain.filter(exchange);
        }

        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        //根据token获取对应的用户id
        String loginId = (String) tokenInfo.getLoginId();
        if (StringUtils.isEmpty(loginId)){
            throw new Exception("未获取到用户信息");
        }
        mutate.header("loginId",loginId);

        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }
}

这是一个网关的登录过滤器,该类实现了全局过滤,整体的目的就是将当前访问的用户的唯一id,放入到转发的请求头中,在后续的服务里,如果需要,直接在请求头中获取即可,下面我来解释一下。

首先实现了GlobalFilter全局过滤器接口,重写filter方法。

ServerWebExchange封装了HTTP请求和响应的上下文,所以我们可以通过getRequest()获取当前的请求,然后我们将该次请求mutate()一个可更改的副本,下面就是判断是否为登录,因为登录是没有用户信息的,我们直接放行。

下面首先获取本次登录的tokeninfo信息,然后获取里面的用户id,然后将其放入到请求副本的请求头中,最后放行。放行的内容:首先构建一个ServerWebExchange的副本,这里封装的请求为,我们刚刚写入用户id的请求request,.build()就是将副本构建。

通过这段代码我们可以完成,通过网关将请求重写,放入用户id,方便转发的微服务进行获取。

上下文对象的封装:

首先我们来改变一下依赖,对不起哈哈哈我的原因,欠考虑了,依赖关系还是有点一点瑕疵,我们需要将controller层的spring-boot-starter-web依赖,转移到common层,然后将以前的日志spring-boot-starter-log4j2依赖删除,否则会冲突。后面可能还会有问题,到时候我们再慢慢整理,我们将公共的依赖全部抽到common层中,这个以后再说。

我们来到catalogue服务中,在commen层中新建以下包和类:

我们需要封装一个登录的上下文对象,代码如下:

package com.yizhiliulianta.movie.common.context;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 登录上下文对象
 */
public class LoginContextHolder {

    private static final InheritableThreadLocal<Map<String,Object>> THREAD_LOCAL = new InheritableThreadLocal<>();

    public static void set(String key,Object val){
        Map<String, Object> map = getThreadLocalMap();
        map.put(key,val);
    }

    public static String getLoginId(){
        return (String) getThreadLocalMap().get("loginId");
    }

    public static Object get(String key){
        Map<String, Object> threadLocalMap = getThreadLocalMap();
        return threadLocalMap.get(key);
    }

    public static void remove(){
        THREAD_LOCAL.remove();
    }

    public static Map<String,Object> getThreadLocalMap(){
        Map<String, Object> map = THREAD_LOCAL.get();
        if (Objects.isNull(map)){
            map = new ConcurrentHashMap<>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }

}

在看这段代码之前,需要大家知道ThreadLocal是什么。

我对ThreadLocal的理解是当前线程的局部变量,其他线程无法访问,InheritableThreadLocal是对ThreadLocal的扩展,允许子线程访问父线程的变量,大家感兴趣可以自行查阅,因为后面我们可能使用多线程,所以这里就提前使用了,目前用ThreadLocal也是可以的。

首先我们new一个InheritableThreadLocal,规定其存储的内容为map集合,方便我们后续通过对应键来获取值。

先来看getThreadLocalMap(),这是实现ThreadLocal里变量唯一不变的关键,首先我们通过ThreadLocal的get方法获取该变量存储的内容,如果返回为null证明为空,证明该变量还没有内容,这时候我们再去new一个map集合,并将这个map集合放入到ThreadLocal里。 

这里的ConcurrentHashMap也是map,但是它是线程安全的,看作map表就好

 剩下的方法就是对map集合的获取和放入了,大家应该都可以看懂了,我就不去详细说了,其中的getLoginId就是在map集合中,通过loginid来获取用户id的方法封装。remove就是将ThreadLocal进行删除,确保在每次任务处理完之后,清理线程上下文,防止数据污染。

拦截器:

下面在common层中新增以下包和类:

我们需要在catalogue服务中实现一个登录拦截器,代码如下:

package com.yizhiliulianta.movie.common.interceptor;

import com.yizhiliulianta.movie.common.context.LoginContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String loginId = request.getHeader("loginId");
        if (StringUtils.isNotBlank(loginId)) {
            LoginContextHolder.set("loginId", loginId);
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginContextHolder.remove();
    }
}

首先我们实现HandlerInterceptor接口,去拦截访问该服务的请求,因为我们在网关中已经将当前用户的id放入到了请求头中,所以我们在进入该服务之前,我们先获取该请求头的信息,从中获取loginid,然后将其放入到我们封装的上下文对象中,然后放行。

工具类:

我们再封装一个工具类,方便我们获取当前用户的id,结构和代码如下:

package com.yizhiliulianta.movie.common.util;

import com.yizhiliulianta.movie.common.context.LoginContextHolder;

/**
 * 用户登录util
 */
public class LoginUtil {

    public static String getLoginId(){
        return LoginContextHolder.getLoginId();
    }


}

mvc拦截器配置:

我们已经将拦截器等准备完毕,最后只需要将拦截器配置到mvc中就可以了,代码和结构如下:

package com.yizhiliulianta.movie.common.config;


import com.yizhiliulianta.movie.common.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * mvc全局处理
 */
@Configuration
public class GlobalConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**");
    }

}

重写addInterceptors方法,调用拦截器链,将我们的拦截器添加到里面,拦截所有路径

测试:

下面我们来测试一下,我们能不能在catalogue服务中获取到当前登录的用户id,我们在domain层新增一条日志,如下:

下面我们使用apipost来进行测试:

 还记得我们的流程吗,先微信获取验证码,然后登录,获取token,然后将token添加到请求头,去进行服务访问

通过网关结课访问catalogue服务,查询电影信息

成功了!我们通过日志可以看到,我们成功获取了当前登录的用户id

Feign的使用:

前言:

首先将catalogue服务的common层的东西,也就是用户上下文打通的包与类,也粘贴到auth服务里,偷懒了,我不再去展示了

在使用之前我先更改一下表结构,给用户表增加一列,为用户的用户名,username是用户id,作为唯一标识,nickname为用户名称

再添加两个方法,一个是修改用户的用户名,一个是查询用户信息

代码我不去细说了,简单的增删改查,大家知道实现了什么就好,看下aippost的效果:

下面我们开始改造我们现在的服务

我们明确一下我们的目标,我们需要在catalogue服务中,获取当前用户的详细信息,也就是说,我们需要在catalogue服务中调取auth服务的方法,这是微服务之间的调用,那么引入我们的Feign

一,依赖:

我们在api层引入需要的依赖,如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.0.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.0.6</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
    </dependencies>

 然后在auth服务的common层中将api引入:

        <dependency>
            <groupId>com.yizhiliulianta</groupId>
            <artifactId>movie-auth-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

二,重构结构:

我们将对应的枚举,和AuthUserDTO放入到api层中,结构如下

然后我们将controller层的导包修改一下,不然会报错,修改成api层的实体类。

三:提供者:

接着我们在api包内添加一个接口,代码如下:

package com.yizhiliulianta.auth.api;

import com.yizhiliulianta.auth.entity.AuthUserDTO;
import com.yizhiliulianta.auth.entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("movie-auth")
public interface UserFeignService {

    @RequestMapping("/user/getUserInfo")
    Result<AuthUserDTO> getUserInfo(@RequestBody AuthUserDTO authUserDTO);

}

在使用feign之前我们需要知道什么rpc,rpc是远程过程调用。

主要作用是,实现调用远程方法就像调用本地方法一样的体验,所以我认为,feign是一个rpc框架。而微服务之间的调用,实际上就是服务与服务之间,通过网络链接,比如http来请求,获取其接口的内容,所以feign或rpc就是简化了这个过程。

下面来看一下feign是如何使用的,首先我们需要构建一个feignapi接口,该接口下的方法是暴露给其他服务使用的,所以auth是提供者。那么在feign中,提供者提供暴露的接口,消费者就是调用其接口的服务。

我们在api接口中为暴露的接口使用requestmapping进行映射,外部调用该方法,实际上就是向映射的地址发起了一次请求。

最后我们在接口上打上@FeignClient注解,里面为auth服务注册到nacos里的名称。

四,消费者:

下面我们在catalogue服务中的common层里,新建一个rpc的包,和调用auth服务的实现类,在实体类包中添加一个userInfo的实体类,结构如下:

首先我们进行一层防腐,比如说我们想获取的只是用户的名称,不需要用户的id等信息,我们在实体包中构建我们想要的用户类,代码如下:

package com.yizhiliulianta.movie.common.entity;

import lombok.Data;

@Data
public class UserInfo {
    
    private String nickName;

}

下面我们开始编写userRpc,代码如下:

package com.yizhiliulianta.movie.common.rpc;


import com.yizhiliulianta.auth.api.UserFeignService;
import com.yizhiliulianta.auth.entity.AuthUserDTO;

import com.yizhiliulianta.auth.entity.Result;
import com.yizhiliulianta.movie.common.entity.UserInfo;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class UserRpc {

    @Resource
    private UserFeignService userFeignService;

    public UserInfo getUserInfo(String userName) {
        AuthUserDTO authUserDTO = new AuthUserDTO();
        authUserDTO.setUserName(userName);
        Result<AuthUserDTO> result = userFeignService.getUserInfo(authUserDTO);
        UserInfo userInfo = new UserInfo();
        if (!result.getSuccess()) {
            return userInfo;
        }
        AuthUserDTO data = result.getData();
        userInfo.setNickName(data.getNickName());
        return userInfo;
    }
}

首先我们将UserFeignService注入,然后新建一个方法来获取用户信息,实际上就是根据用户的id去封装成一个auth服务需要的userdto,然后发起请求,会返回给我们一个用户信息结果,我们再对结果进行一次封装,将我们需要的userinfo返回。

最后我们需要在启动类上加上注解

@EnableFeignClients(basePackages = "com.yizhiliulianta")

通过该注解,启用 Feign 客户端的功能。basePackages 属性用于指定要扫描的包,Spring 会在这些包中查找使用了 @FeignClient 注解的接口

五:测试:

下面我们还是在catalogue服务的domain层中进行测试,代码如下:

package com.yizhiliulianta.movie.domain.service.impl;

import com.alibaba.fastjson.JSON;
import com.yizhiliulianta.movie.common.entity.UserInfo;
import com.yizhiliulianta.movie.common.rpc.UserRpc;
import com.yizhiliulianta.movie.common.util.LoginUtil;
import com.yizhiliulianta.movie.domain.bo.MovieBO;
import com.yizhiliulianta.movie.domain.convert.MovieBOConverter;
import com.yizhiliulianta.movie.domain.service.MovieDomainServcie;
import com.yizhiliulianta.movie.infra.entity.TMovie;
import com.yizhiliulianta.movie.infra.service.TMovieService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class MovieDomainServiceImpl implements MovieDomainServcie{

    @Resource
    private TMovieService movieService;

    @Resource
    private UserRpc userRpc;

    @Override
    public MovieBO selectMovie(MovieBO movieBO) {
        TMovie tmovie = MovieBOConverter.INSTANCE.convertBoToMovie(movieBO);
        TMovie movie = movieService.queryById(tmovie.getId());

        String loginId = LoginUtil.getLoginId();
        UserInfo userInfo = userRpc.getUserInfo(loginId);
        log.info("用户为:{}",userInfo.getNickName());
        
        MovieBO bo = MovieBOConverter.INSTANCE.convertToMovieBO(movie);
        if (log.isInfoEnabled()) {
            log.info("MovieController.selectMovie.bo:{}",
                    JSON.toJSONString(bo));
        }
        return bo;
    }
}

没什么变化,就是将UserRpc注入,然后调用其方法,通过satoken框架获取登录的id,然后传入,获取其用户的名称

下面我们启动所有服务,打开apipost,进行测试,查看是否能成功调用:

我们去后台看一下日志:

调用成功!到这里我们的微服务的调用就结束了 

微服务的使用到这里就差不多了,后面应该是定时任务,消息队列,和多线程的一些使用

谢谢大家的观看!

地址:

gitee:movieCloud: 微服务的练习

movieCloud: 微服务的练习 (gitee.com)

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值