PS:源码已上传Github, 欢迎指教。https://github.com/shileishmily/spring-cloud-x.git
之前已经讲过zuul网关搭建和网关Filter使用,参考下面两篇文章。
本篇主要Zuul和Dubbo如何集成,因为笔者所在的公司既有Dubbo服务,又有SpringCloud服务。对外(公网)提供服务的时候,不可能把dubbo服务再改造成Http协议,一是成本大,二是风险高。所以我们采用Zuul网关实现SpringCloud和Dubbo服务分发。在Zuul网关之上再挂Nginx对外提供服务。
本篇演示离不开zookeeper,zkui,Dubbo Admin。关于这三个应用的安装请参考下方链接,本文不做重点介绍。
zookeeper,zkui安装参考:Zookeeper 注册中心安装
Dubbo Admin安装参考:https://github.com/apache/dubbo-admin
1、Dubbo接口定义
在x-demo-service-api模块新建一个HelloDubboService接口
package com.x.demo.api; public interface HelloDubboService { void sayHello(); String sayHi(String name); }
2、创建一个名称为x-demo-dubbo-provider的模块
3、gradle依赖
dependencies { compile project(":x-demo-service-api") compile(group: 'org.apache.dubbo', name: 'dubbo-spring-boot-starter', version: '2.7.8') { exclude(module: 'slf4j-log4j12') } compile(group: 'org.apache.dubbo', name: 'dubbo-dependencies-zookeeper', version: '2.7.8') { exclude(module: 'slf4j-log4j12') } }
4、bootstrap.yml配置文件
server: port: 6060 dubbo: application: name: x-demo-dubbo-provider registry: address: zookeeper://localhost:2181 protocol: zookeeper protocol: name: dubbo port: 20880 scan: base-packages: com.x.dubbo.provider.service eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ enabled: false
5、创建HelloDubboService实现类,代码如下
这里我们采用注解声明式配置,实现类上加@DubboService
/** * @author Leo */ @Slf4j @DubboService(version = "1.0.0", interfaceClass = HelloDubboService.class, methods = { @Method(name = "sayHello") }) public class HelloDubboServiceImpl implements HelloDubboService { @Override public void sayHello() { log.info("Hello"); } @Override public String sayHi(String name) { log.info("Hello: {}", name); return "Hello: " + name; } }
6、创建启动类
注意必须加@EnableDubbo注解,否则服务无法注册到zookeeper。
/** * @author Leo */ @EnableDubbo @SpringBootApplication public class DubboProviderApplication { public static void main(String[] args) { SpringApplication.run(DubboProviderApplication.class, args); } }
到此dubbo服务提供者完成。下面开始改造spring-cloud-gateway模块
7、创建路由Filter
package com.x.gateway.zuul.filter; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.Feature; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import com.x.gateway.zuul.util.DubboCallbackUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.LinkedHashMap; import java.util.List; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTE_TYPE; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_RESPONSE_FILTER_ORDER; /** * Dubbo泛化调用 * * @author Leo */ @Slf4j @Component public class DubboForwardFilter extends ZuulFilter { @Override public String filterType() { return ROUTE_TYPE; } @Override public int filterOrder() { return SEND_RESPONSE_FILTER_ORDER - 2; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString()); if ("POST".equals(request.getMethod())) { LinkedHashMap param = null; try (InputStream inputStream = request.getInputStream()) { String body = StreamUtils.copyToString(inputStream, Charset.forName("UTF-8")); log.info("原始请求参数:{}", body); param = JSONObject.parseObject(body, LinkedHashMap.class, Feature.OrderedField); String interfaceName = (String) param.get("interfaceName"); String method = (String) param.get("method"); String version = (String) param.get("version"); String address = (String) param.get("address"); String json = param.get("param").toString(); List<Object> paramList = JSONObject.parseArray(json); if (!StringUtils.isEmpty(interfaceName) && !StringUtils.isEmpty(method)) { Object responseTxt = DubboCallbackUtil.invoke(interfaceName, method, paramList, address, version); requestContext.setSendZuulResponse(true); requestContext.setResponseStatusCode(HttpStatus.OK.value()); requestContext.setResponseBody((String) responseTxt); } } catch (IOException e) { log.error("dubbo泛化调动异常", e); } } return null; } }
8、Dubbo泛化调用工具类
/** * @author Leo */ public class DubboCallbackUtil { private static Logger logger = LogManager.getLogger(DubboCallbackUtil.class); /** * 当前应用的信息 */ private static ApplicationConfig application = new ApplicationConfig(); /** * 注册中心信息缓存 */ private static Map<String, RegistryConfig> registryConfigCache = new ConcurrentHashMap<>(); /** * 各个业务方的ReferenceConfig缓存 */ private static Map<String, ReferenceConfig> referenceCache = new ConcurrentHashMap<>(); static { application.setName("spring-cloud-gateway"); } /** * 获取注册中心信息 * * @param address zk注册地址 * @param group dubbo服务所在的组 * @return */ private static RegistryConfig getRegistryConfig(String address, String group, String version) { String key = address + "-" + group + "-" + version; RegistryConfig registryConfig = registryConfigCache.get(key); if (null == registryConfig) { registryConfig = new RegistryConfig(); if (StringUtils.isNotEmpty(address)) { registryConfig.setAddress(address); } if (StringUtils.isNotEmpty(version)) { registryConfig.setVersion(version); } if (StringUtils.isNotEmpty(group)) { registryConfig.setGroup(group); } registryConfigCache.put(key, registryConfig); } return registryConfig; } private static ReferenceConfig getReferenceConfig(String interfaceName, String address, String group, String version) { String referenceKey = interfaceName; ReferenceConfig referenceConfig = referenceCache.get(referenceKey); if (null == referenceConfig) { try { referenceConfig = new ReferenceConfig<>(); referenceConfig.setApplication(application); referenceConfig.setRegistry(getRegistryConfig(address, group, version)); Class interfaceClass = forName(interfaceName); referenceConfig.setInterface(interfaceClass); if (StringUtils.isNotEmpty(version)) { referenceConfig.setVersion(version); } referenceConfig.setGeneric(true); //referenceConfig.setUrl("dubbo://10.1.50.167:20880/com.test.service.HelloService"); referenceCache.put(referenceKey, referenceConfig); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return referenceConfig; } public static Object invoke(String interfaceName, String methodName, List<Object> paramList, String address, String version) { ReferenceConfig reference = getReferenceConfig(interfaceName, address, null, version); if (null != reference) { GenericService genericService = (GenericService) reference.get(); if (genericService == null) { logger.debug("GenericService 不存在:{}", interfaceName); return null; } Object[] paramObject = null; if (!CollectionUtils.isEmpty(paramList)) { paramObject = new Object[paramList.size()]; for (int i = 0; i < paramList.size(); i++) { paramObject[i] = paramList.get(i); } } Object resultParam = genericService.$invoke(methodName, getMethodParamType(interfaceName, methodName), paramObject); return resultParam; } return null; } public static String[] getMethodParamType(String interfaceName, String methodName) { try { //创建类 Class<?> class1 = Class.forName(interfaceName); //获取所有的公共的方法 Method[] methods = class1.getMethods(); for (Method method : methods) { if (method.getName().equals(methodName)) { Class[] paramClassList = method.getParameterTypes(); String[] paramTypeList = new String[paramClassList.length]; int i = 0; for (Class className : paramClassList) { paramTypeList[i] = className.getTypeName(); i++; } return paramTypeList; } } } catch (Exception e) { e.printStackTrace(); } return null; } }
9、bootstrap.yml配置修改
注意标红的地方,加上这个配置,不管我们有多少个dubbo服务,都通过统一URLhttp://localhost:9001/dubbo请求到自己定路由DubboForwardFilter。
spring: application: name: spring-cloud-gateway server: port: 9001 eureka: client: service-url: defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/ management: endpoints: web: exposure: include: '*' zuul: routes: x-demo-dubbo-provider: /dubbo/**
10、依次启动zk,x-demo-dubbo-provider,spring-cloud-gateway
通过PostMan访问Zuul网关,成功收到Dubbo服务返回:“Hello Leo”。
请求地址:http://localhost:9001/dubbo
请求参数:
{ "interfaceName":"com.x.demo.api.HelloDubboService", "method":"sayHi", "version":"1.0.0", "param":["Leo"], "address":"zookeeper://localhost:2181" }
当然如果要应用到生产,还需要细化打磨代码。但是有一点可以肯定,此方案在性能上是没有问题,因为实践出真知!!!