一个简单的请求转发网关,可以用做中台网关,不喜勿喷

项目地址:https://github.com/lcy19930619/api-center

适用场景

1、有很多后端服务,
2、尚未接入或无法接入分布式框架,
3、需要集中统一管理接口

整体设计思路:

转发网关需要分成以下五部分来考虑

1. 客户端

在网关上,需要将所有的请求转发给每个真实的服务,所以网关是客户端,考虑到客户端的转发性能问题,直接使用的是webflux+netty client pool

2. 服务端

服务端需要放在每个服务中,应尽可能减少侵入性,而且配置尽可能少,便于集成

对spring的支持

支持spring boot和spring mvc
spring boot可以使用spi机制进行支持

package net.jlxxw.apicenter.facade.runner;

import net.jlxxw.apicenter.facade.properties.ApiCenterClientProperties;
import net.jlxxw.apicenter.facade.remote.AbstractRemoteManager;
import net.jlxxw.apicenter.facade.scanner.MethodScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author zhanxiumei
 */
@Component
public class ApiCenterRunner implements ApplicationContextAware, ApplicationRunner {

    private static final Logger logger = LoggerFactory.getLogger(ApiCenterRunner.class);
    /**
     * Spring上下文
     */
    private ApplicationContext applicationContext;

    @Autowired
    private MethodScanner methodScanner;

    @Autowired
    private ApiCenterClientProperties apiCenterClientProperties;

    @Autowired
    private AbstractRemoteManager remoteManager;
    /**
     * boot启动完毕后会自动回调这个方法
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) {
        // 扫描全部bean
        logger.info("begin scan method");
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = applicationContext.getBean(beanDefinitionName);
            methodScanner.scanMethod(bean);
        }
        logger.info("method registry done");

        // 初始化远程执行相关全部内容
        remoteManager.init(apiCenterClientProperties);


    }

    /**
     * 获取Spring上下文
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

spring mvc机制可以使用事件多播器机制进行支持

package net.jlxxw.apicenter.facade.runner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.event.ContextRefreshedEvent;

/**
 * @author zhanxiumei
 */
@ComponentScan("net.jlxxw.apicenter.facade")
public class SpringMvcSupport implements ApplicationContextAware,ApplicationListener<ContextRefreshedEvent> {

    private static final Logger logger = LoggerFactory.getLogger(SpringMvcSupport.class);

    private ApplicationContext applicationContext;

    @Autowired
    private ApiCenterRunner apiCenterRunner;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }


    /**
     * Handle an application event.
     *
     * @param event the event to respond to
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        try {
            Class.forName("org.springframework.boot.autoconfigure.SpringBootApplication");
            logger.info("environment is spring boot");
        } catch (ClassNotFoundException e) {
            // 如果加载失败,则说明非boot环境,需要启动mvc支持
            apiCenterRunner.setApplicationContext(applicationContext);
            // 执行启动网关内容
            logger.info("environment is spring mvc ,enable spring mvc support");
            apiCenterRunner.run(null);
        }
    }
}

方法注册表的实现

利用对spring的支持,在spring ioc完成启动时,扫描全部的bean,将bean的实例对象、具体方法、方法入参、返回值,以及方法标注的serviceCode具体值进行存储,等待远程客户端发出请求,并反射执行此方法

package net.jlxxw.apicenter.facade.scanner;

import net.jlxxw.apicenter.facade.annotation.RemoteRegister;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 方法扫描
 * @author zhanxiumei
 */
@Component
public class MethodScanner {

    private static  final Logger logger = LoggerFactory.getLogger(MethodScanner.class);
    /**
     * 方法本地注册表
     */
    private static final Map<String,MethodInfo> REGISTRY_TABLE = new ConcurrentHashMap<>(16);

    /**
     * 将扫描到到方法注册到注册表中
     * @param object 实例对象
     * @param method 调用的方法
     * @param serviceCode 方法唯一识别码
     * @param parameterTypes 方法参数类型列表
     * @param hasReturn 是否具有返回值
     */
    private void registry(Object object, Method method,String serviceCode,Class[] parameterTypes,boolean hasReturn,String[] methodParamNames){
        MethodInfo methodInfo = new MethodInfo();
        methodInfo.setParameterTypes(parameterTypes);
        methodInfo.setMethod(method);
        methodInfo.setObject(object);
        methodInfo.setHasReturn(hasReturn);
        methodInfo.setMethodParamNames(methodParamNames);
        REGISTRY_TABLE.put(serviceCode,methodInfo);
        logger.info("registry method "+method);
    }

    /**
     * 扫描方法,并检测是否合规
     * @param bean spring bean
     */
    public void scanMethod(Object bean){
        Class clazz;
        if(AopUtils.isAopProxy(bean)){
            clazz = AopUtils.getTargetClass(bean);
        }else{
            clazz = bean.getClass();
        }
        // 获取全部声明的方法
        Method[] declaredMethods = clazz.getDeclaredMethods();
        if(Objects.nonNull(declaredMethods)){
            for (Method declaredMethod : declaredMethods) {
                // 如果方法包含指定的注解,则进行相关解析
                if(declaredMethod.isAnnotationPresent(RemoteRegister.class)){
                    RemoteRegister annotation = declaredMethod.getAnnotation(RemoteRegister.class);
                    String serviceCode = annotation.serviceCode();
                    if(StringUtils.isBlank(serviceCode)){
                        // 注解中的 buc code 不能为空
                        throw new IllegalArgumentException("method:" + declaredMethod +" serviceCode is not null");
                    }
                    if(REGISTRY_TABLE.containsKey(serviceCode)){
                        // 注解中的 buc code 不能重复
                        MethodInfo methodInfo = REGISTRY_TABLE.get(serviceCode);
                        throw new IllegalArgumentException("method:" + declaredMethod + " serviceCode exists,please check "+methodInfo.getMethod().getName());
                    }

                    // 获取返回值类型
                    Class<?> returnType = declaredMethod.getReturnType();
                    // 获取参数列表
                    Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
                    if(parameterTypes.length >0){
                        for (Class<?> parameterType : parameterTypes) {
                            if(parameterType.isArray() || parameterType.isEnum()){
                                throw new IllegalArgumentException("method: "+declaredMethod + "param is not support,not support type:array,enum");
                            }
                        }
                    }
                    // 获取全部方法参数名称
                    LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
                    String[] parameterNames = localVariableTableParameterNameDiscoverer.getParameterNames(declaredMethod);

                    registry(bean,declaredMethod,serviceCode,parameterTypes,"void".equals(returnType.getName()),parameterNames);
                }
            }
        }
    }

    /**
     * 根据方法注解编码,获取相关执行的方法
     * @param serviceCode
     * @return
     */
    public MethodInfo getMethod(String serviceCode){
        return REGISTRY_TABLE.get(serviceCode);
    }
}

3. 注册中心

能保证服务端和客户端的注册与发现即可
在服务端启动完毕后,自动创建zookeeper的临时节点,节点列表如下

/api-center    // 转发网关的永久节点
/api-center/applicationName/ip:port   // 服务项目的临时节点
示例:
/api-center/demo1/182.168.1.1:1001
/api-center/demo1/182.168.1.2:1001
package net.jlxxw.apicenter.facade.remote;

import net.jlxxw.apicenter.facade.constant.ApiCenterConstant;
import net.jlxxw.apicenter.facade.exception.ApiCenterException;
import net.jlxxw.apicenter.facade.netty.NettyProxy;
import net.jlxxw.apicenter.facade.properties.ApiCenterClientProperties;
import net.jlxxw.apicenter.facade.utils.IPAddressUtils;
import net.jlxxw.apicenter.facade.utils.ZookeeperUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author zhanxiumei
 */
@Component
public class RemoteManager extends AbstractRemoteManager {

    private static final Logger logger = LoggerFactory.getLogger(RemoteManager.class);
    @Autowired
    private ZookeeperUtils zookeeperUtils;

    @Value("${spring.application.name}")
    private String applicationName;

    @Autowired
    private NettyProxy nettyProxy;
    /**
     * 向注册中心注册
     *
     * @param apiCenterClientProperties
     *
     * @throws ApiCenterException
     */
    @Override
    protected void registryCenter(ApiCenterClientProperties apiCenterClientProperties) throws ApiCenterException {
        if (StringUtils.isBlank( applicationName )) {
            throw new IllegalArgumentException( "application name is not null" );
        }
        if (!zookeeperUtils.existsNode(ApiCenterConstant.PARENT_NODE)) {
            // 如果api center主节点不存在,创建节点
            zookeeperUtils.createOpenACLPersistentNode(ApiCenterConstant.PARENT_NODE, "".getBytes());
        }

        String parentPath = ApiCenterConstant.PARENT_NODE + "/" + applicationName ;
        if (!zookeeperUtils.existsNode( parentPath )) {
            // 如果节点不存在,创建节点
            zookeeperUtils.createOpenACLPersistentNode( parentPath, "".getBytes() );
        }

        String serverIp = apiCenterClientProperties.getServerIp();


        if(StringUtils.isBlank(serverIp)){
            String ipAddress = IPAddressUtils.getIpAddress();
            logger.info("server ip not found,enable automatic acquisition,ip address :"+ipAddress);
        }


        parentPath = parentPath + "/" + serverIp + ":" + apiCenterClientProperties.getPort();
        if (!zookeeperUtils.existsNode( parentPath )) {
            // 如果节点不存在,创建节点
            zookeeperUtils.createOpenACLEphemeralNode( parentPath, "".getBytes() );
        }

    }

    /**
     * 初始化通信框架
     *
     * @param apiCenterClientProperties
     */
    @Override
    protected void initNetty(ApiCenterClientProperties apiCenterClientProperties) throws ApiCenterException {
        nettyProxy.initProxy( apiCenterClientProperties );
    }

    /**
     * 关闭代理对象
     */
    @Override
    public void closeProxy() {

    }
}

4. 安全认证

调用接口需要进行安全认证,比如用户身份识别一类的

package net.jlxxw.apicenter.service.impl;

import com.alibaba.fastjson.JSON;
import net.jlxxw.apicenter.constant.ResultCodeEnum;
import net.jlxxw.apicenter.dao.ServiceInfoDAO;
import net.jlxxw.apicenter.domain.ServiceInfoDO;
import net.jlxxw.apicenter.dto.ForwardingDTO;
import net.jlxxw.apicenter.facade.constant.ApiCenterConstant;
import net.jlxxw.apicenter.facade.dto.RemoteExecuteReturnDTO;
import net.jlxxw.apicenter.facade.enums.MethodFlagEnum;
import net.jlxxw.apicenter.facade.impl.netty.NettyClient;
import net.jlxxw.apicenter.facade.param.RemoteExecuteParam;
import net.jlxxw.apicenter.facade.utils.ZookeeperUtils;
import net.jlxxw.apicenter.intergration.buc.BucClient;
import net.jlxxw.apicenter.service.ForwardingService;
import net.jlxxw.apicenter.vo.ApiCenterResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.Random;

/**
 * 2020-10-18 12:08
 *
 * @author LCY
 */
@Service
public class ForwardingServiceImpl implements ForwardingService {
    private static final Logger logger =LoggerFactory.getLogger(ForwardingServiceImpl.class);
    @Resource
    private ServiceInfoDAO serviceInfoDAO;
    @Autowired
    private ZookeeperUtils zookeeperUtils;
    @Autowired
    private NettyClient nettyClient;
    @Autowired
    private BucClient bucClient;
    /**
     * 处理网关转发服务
     *
     * @param dto 前端页面入参对象
     *
     * @return 网关处理结果
     */
    @Override
    public Mono<ApiCenterResult> forward(ForwardingDTO dto) {

        /*
            判断service code 是否正确
         */
        ServiceInfoDO serviceInfoDO = serviceInfoDAO.findByServiceCode( dto.getServiceCode() );
        if (Objects.isNull( serviceInfoDO )) {
            return Mono.just( ApiCenterResult.failed( ResultCodeEnum.SERVICE_CODE_IS_NOT_EXISTS ) );
        }

        /*
            检测服务是否在线
         */
        String appName = serviceInfoDO.getAppName();
        List<String> nodes = zookeeperUtils.listChildrenNodes( ApiCenterConstant.PARENT_NODE + "/" + appName );
        if (CollectionUtils.isEmpty( nodes )) {
            return Mono.just( ApiCenterResult.failed( ResultCodeEnum.SERVER_IS_OFFLINE ) );
        }

        /*
            todo 网关接口鉴权
         */
        if (!bucClient.auth( "", dto.getServiceCode() )) {
            return Mono.just( ApiCenterResult.failed( ResultCodeEnum.SERVER_IS_OFFLINE ) );
        }

        /*
            随机获取一个服务节点
         */
        Random random = new Random();
        int index = random.nextInt( nodes.size() );
        String address = nodes.get( index );
        String[] split = address.split( ":" );

        /*
            执行远程方法
         */
        RemoteExecuteParam remoteExecuteParam = new RemoteExecuteParam();
        remoteExecuteParam.setServiceCode( dto.getServiceCode() );
        remoteExecuteParam.setMethodParamJson( JSON.toJSONString( dto.getRequestParam() ) );
        remoteExecuteParam.setMethodFlag( MethodFlagEnum.NORMAL.name() );
        try {
            remoteExecuteParam.setIp( split[0] );
            remoteExecuteParam.setPort( Integer.valueOf( split[1] ) );
            RemoteExecuteReturnDTO result = nettyClient.send( remoteExecuteParam );
            return Mono.just( ApiCenterResult.success( result ) );
        } catch (Exception e) {
            logger.error("remote method execute failed!!!",e);
            return Mono.just( ApiCenterResult.failed( ResultCodeEnum.REMOTE_EXECUTE_FAILED ) );
        }
    }
}

5. 网络通信

性能问题,还没想出来怎么解决netty异步获取返回值这个问题,直接使用的wait 和notify

package net.jlxxw.apicenter.facade.impl.netty.impl;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelOption;
import io.netty.channel.DefaultChannelPromise;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.pool.AbstractChannelPoolMap;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.pool.FixedChannelPool;
import io.netty.channel.pool.SimpleChannelPool;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.Promise;
import net.jlxxw.apicenter.facade.dto.RemoteExecuteReturnDTO;
import net.jlxxw.apicenter.facade.impl.netty.ClientHandler;
import net.jlxxw.apicenter.facade.impl.netty.NettyClient;
import net.jlxxw.apicenter.facade.param.RemoteExecuteParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * @author zhanxiumei
 */
@Service
public class NettyClientImpl  implements NettyClient  {

    private static final Logger logger = LoggerFactory.getLogger(NettyClientImpl.class);
    //管理以ip:端口号为key的连接池   FixedChannelPool继承SimpleChannelPool,有大小限制的连接池实现
    private static AbstractChannelPoolMap<InetSocketAddress, FixedChannelPool> poolMap;

    /**
     * key channel ID,value 请求参数
     */
    private static Map<String,RemoteExecuteParam> map = new ConcurrentHashMap<>();
    //启动辅助类 用于配置各种参数
    private  Bootstrap bootstrap =new Bootstrap();

    public NettyClientImpl(){
        ClientHandler clientHandler = new ClientHandler( this );
        bootstrap.group(new NioEventLoopGroup())
                .channel( NioSocketChannel.class)
                .option( ChannelOption.TCP_NODELAY,true);
        poolMap = new AbstractChannelPoolMap<InetSocketAddress, FixedChannelPool>() {
            @Override
            protected FixedChannelPool newPool(InetSocketAddress inetSocketAddress) {
                ChannelPoolHandler handler = new ChannelPoolHandler() {
                    //使用完channel需要释放才能放入连接池
                    @Override
                    public void channelReleased(Channel ch) throws Exception {

                    }
                    //当链接创建的时候添加channel handler,只有当channel不足时会创建,但不会超过限制的最大channel数
                    @Override
                    public void channelCreated(Channel ch) throws Exception {
                        logger.info("channelCreated. Channel ID: " + ch.id());
                        ch.pipeline().addLast(new ObjectEncoder());
                        ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE,  ClassResolvers.weakCachingConcurrentResolver(null)));
                        ch.pipeline().addLast(clientHandler);//添加相应回调处理
                    }
                    //获取连接池中的channel
                    @Override
                    public void channelAcquired(Channel ch) throws Exception {

                    }
                };
                return new FixedChannelPool(bootstrap.remoteAddress(inetSocketAddress), handler, 5); //单个服务端连接池大小
            }
        };
    }
    /**
     * 向远程服务发送相关数据
     *
     * @param param
     * @return
     */
    @Override
    public RemoteExecuteReturnDTO send(RemoteExecuteParam param) throws InterruptedException {
        String ip = param.getIp();
        Integer port = param.getPort();
        InetSocketAddress address = new InetSocketAddress(ip, port);
        final SimpleChannelPool pool = poolMap.get(address);
        final Future<Channel> future = pool.acquire();
        future.addListener( (FutureListener<Channel>) arg0 -> {
            if (future.isSuccess()) {
                Channel ch = future.getNow();
                ChannelId id = ch.id();
                String tempId = id.toString();
                param.setChannelId( tempId );
                map.put( tempId,param );
                ch.writeAndFlush(param);
                synchronized (param) {
                    //因为异步 所以不阻塞的话 该线程获取不到返回值
                    //放弃对象锁 并阻塞等待notify
                    try {
                        // 超时时间十秒,到时自动释放
                        param.wait(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //放回去
                pool.release(ch);
            }
        } );

        return param.getResult();
    }



    /**
     * 根据channelId 获取执行参数
     *
     * @param channelId
     * @return
     */
    @Override
    public RemoteExecuteParam getRemoteExecuteParam(String channelId) {
        return map.get(channelId);
    }

    /**
     * 当指定的服务下线后,移除此通道
     *
     * @param ip
     * @param port
     */
    @Override
    public void removeChannel(String ip, Integer port) {
        InetSocketAddress address = new InetSocketAddress(ip, port);
        poolMap.remove( address );
    }

}

package net.jlxxw.apicenter.facade.impl.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.jlxxw.apicenter.facade.dto.RemoteExecuteReturnDTO;
import net.jlxxw.apicenter.facade.impl.netty.impl.NettyClientImpl;
import net.jlxxw.apicenter.facade.param.RemoteExecuteParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 读取服务器返回的响应信息
 * @author zhanxiumei
 *
 */
public class ClientHandler extends ChannelInboundHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(ClientHandler.class);

    private NettyClient nettyClient;

    public ClientHandler(NettyClient nettyClient) {
        this.nettyClient = nettyClient;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)  {
        RemoteExecuteReturnDTO result = null;
        String channelId = null;
        RemoteExecuteParam remoteExecuteParam = null;
        try {
            result = (RemoteExecuteReturnDTO)msg;
            channelId = result.getChannelId();
            remoteExecuteParam =nettyClient.getRemoteExecuteParam(channelId);
        } catch (Exception e){
            RemoteExecuteReturnDTO obj = new RemoteExecuteReturnDTO();
            obj.setSuccess( false );
            obj.setMessage( "远程执行产生位置异常!" );
            logger.error( "远程执行产生位置异常!",e );
        }finally {
            synchronized (remoteExecuteParam){
                remoteExecuteParam.setResult( result);
                remoteExecuteParam.notify();
            }
        }
    }

    // 数据读取完毕的处理
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        logger.info("客户端读取数据完毕");
        ctx.flush();
    }

    // 出现异常的处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error("client 读取数据出现异常",cause);
        ctx.close();
    }

}

使用说明

任意client为spring 项目,需要支持远程调用的方法,需要加入@RemoteRegister 注解,并将此bean交由IOC进行管理
使用方法:
1、api-center项目 facade 模块执行 mvn clean instaill 安装到本地仓库,有条件的可以上传到私服
2、在新项目中,加入 facade 依赖,坐标参考api-center-facade工程pom文件
3、在指定的方法加上@RemoteRegister,并配置serviceCode具体值
4、apicenter需要配置注册中心,数据库等基础数据信息,同时添加项目的基本信息到数据表中
5、client需要和api-center处于同一个zookeeper环境中
注意事项
client:
1、方法入参目前仅支持对象,不支持基本数据类型和String等,需要自定义结构体
2、方法返回值不支持数据结构,如Map,List等,需要使用对象
3、client的基本配置可以在yml中输入 api-center进行联想

在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42321034/article/details/111057755