统一接口平台(有演示地址)
2018-12-03 21:34:35
1产品介绍
前端应用系统通过统一接口平台获取数据,不直接与外部系统接口打交道。统一接口平台通过多种方式与外部系统联接获取数据并向各前端应用系统提供各种数据格式包,将外部系统有效地隔离在业务系统之外。前端应用系统需要请求的外部接口需要在统一接口平台注册,开放。每次访问都会被有效的记录,实行监管。
2应用范围
前后端分离,跨域,缓存策略,接口管控,服务组装,负载均衡等。
3技术描述
应用包:servlet,ehcache,memcached/redis,http协议,http连接池,apache下httpclient,IOUtils,装饰模式,责任链模式,享元模式。
后台包:Spring MVC,Mybatis。
4架构描述
5设计欣赏
6项目工程
1.jdm-proxyadminweb:统一接口平台后台
2.jdm-proxyappweb:统一接口平台应用
3.jdm-proxycore:统一接口平台核心包
7使用手册
1.部署统一接口平台应用包到应用服务器。如部署了两台tomcat服务器,分别是192.168.1.1和192.168.1.2。
2.配置nginx反向代理,路由映射应用服务器地址。如:
upstream www.abc.com{
server 192.168.1.1:8080;
server 192.168.1.2:8080;
}
location /proxy/ {
proxy_pass http://www.abc.com;
}
3.使用统一接口平台方法:路由地址+路由名称+p+接口地址,如:
http://www.abc.com/proxy/p/shop/checkUrl.json
4.相关服务接口地址都是在统一接口平台后台配置,通过推送xml文件到每台统一接口平台应用包所在的服务器。xml文件格式如下:
8核心代码
统一接口平台应用核心逻辑图:
一 ProxyServlet.java
get请求入口:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String requestId=UUID.randomUUID().toString(); try{ log.info("start to proxy init "+requestId); ProxyBean proxy=new GetProxy(request,response,ProxyConfig.getInstance()); proxy.setAttribute(ProxyConstants.PROXY_BEAN_ID, requestId); log.info("finished to proxy init "+requestId); this.handleProxy(request,response,proxy); }catch(ProxyException e){ e.printStackTrace(); response.getWriter().println(e); } }
post请求入口:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ String requestId=UUID.randomUUID().toString(); try{ log.info("start to proxy init "+requestId); ProxyBean proxy=new PostProxy(request,response,ProxyConfig.getInstance()); proxy.setAttribute(ProxyConstants.PROXY_BEAN_ID, requestId); log.info("finished to proxy init "+requestId); this.handleProxy(request,response,proxy); }catch(ProxyException e){ e.printStackTrace(); response.getWriter().println(e); } }
put请求和delete请求类似,省略....
private void handleProxy(HttpServletRequest request, HttpServletResponse response,ProxyBean proxy) throws IOException{ try{ proxy.getFilterChain().doFilter(proxy); }catch(ProxyException e){ e.printStackTrace(); }catch(IOException e){ e.printStackTrace(); }finally{ if(proxy!=null && proxy.getHttpMethod()!=null){ proxy.getHttpMethod().releaseConnection(); } proxy.getFilterChain().clear(); } } public void init(ServletConfig config) throws ServletException{ super.init(config); ProxyConfig.getInstance().create(); }
二 ProxyBean.java
相关成员变量和构造函数
private Map context =new HashMap(); private ProxyFilterChain filterChain; private HttpServletRequest request; private HttpServletResponse response; private HttpMethod httpMethod; private RequestBean requestBean; private ProxyConfig config; //post put http 请求中来来自请求的输入字符串 private String requestString;//post put http 请求中来来自请求的输入字符串 //http client执行方法后,获取的响应流。 //临时变量,方便其它过滤器重复使用。因为流只能使用一次,因此将其类为ByteArrayInputStream private ByteArrayInputStream resultStream; public ProxyBean(HttpServletRequest request,HttpServletResponse response,ProxyConfig config) throws ProxyException{ if(config==null ||!config.isInitialized()){ throw new ProxyException(HttpStatus.SC_INTERNAL_SERVER_ERROR,"Failed to init the proxy config"); } this.request=request; this.response=response; this.config=config; this.doInit(); }
获取url相关参数,并添加过滤器
protected void doInit() throws ProxyException{ this.initPolicy(); //初始化ProxyFilterChain,增加policy的proxy filter ListcustomFilters=new ArrayList(); ProxyFilterChain pfc=ProxyFilterChainFactory.getInstance().create(customFilters); this.filterChain=pfc; } private void initPolicy() throws ProxyException{ String mappingPath=request.getPathInfo();//接口服务方法名 log.info("mappingPath==="+mappingPath); ServiceModel serviceModel=EhCache.getInstance(Constant.EHCACHE).get(Constant.PROXY_SERVICE, mappingPath); if(serviceModel==null){ throw new ProxyException("the interface service:"+mappingPath+" does not exist"); } String targetPath=serviceModel.getUrl(); String queryString=request.getQueryString();//接口服务参数 //过滤url中系统保留的param if(queryString!=null){ queryString=stripKnownParamters(queryString); } try{ RequestBean requestBean=new RequestBean(targetPath+mappingPath,queryString); this.setRequestBean(requestBean); }catch(MalformedURLException e){ e.printStackTrace(); } }
最终执行请求
public final void doProxy() throws ProxyException,IOException{ this.doBeforeProxy(); this.doExecute(); this.doAfterProxy(); }
前置逻辑
protected void doBeforeProxy() throws ProxyException,IOException{ //1.处理http header Enumerationem=request.getHeaderNames(); while(em.hasMoreElements()){ String headerName=em.nextElement(); String headerValue=request.getHeader(headerName); this.httpMethod.addRequestHeader(headerName,headerValue); } //2.处理http cookies if(request.getHeader("Cookie")!=null){ String rawCookiesString=request.getHeader("Cookie"); if((rawCookiesString!=null) && (rawCookiesString.length()!=0)){ StringBuffer cookieBuffer= new StringBuffer(); String [] cookieParts=rawCookiesString.split(";"); for(int i=0;i<cookieParts.length;i++){ String plainCookie=cookieParts[i].trim(); Cookie cookie=new Cookie(plainCookie); String cookieName=cookie.getName(); String cookieValue=cookie.getValue(); if(cookieBuffer.length()!=0){ cookieBuffer.append(";"); } cookieBuffer.append(cookieName).append("=").append(cookieValue); } System.out.println("Cookie="+cookieBuffer.toString()); httpMethod.addRequestHeader("Cookie",cookieBuffer.toString()); } } }
执行
public final void doExecute() throws ProxyException,IOException{ long before=System.currentTimeMillis(); int statusCode=HttpStatus.SC_INTERNAL_SERVER_ERROR; try{ statusCode=config.acquireHttpClient().executeMethod(httpMethod); String mappingPath=request.getPathInfo(); //将错误信息打印到日志,便于问题定位 if(statusCode>=400){ log.info("Faild to execute the proxy,http status code is "+statusCode+" "+this.getRequestBean()); } }catch(UnknownHostException e){ statusCode=HttpStatus.SC_NOT_FOUND; throw new ProxyException(HttpStatus.SC_NOT_FOUND,"The specified target host is unknown! The target url is "+this.getRequestBean(),e); }catch(SocketException e){ statusCode=HttpStatus.SC_NOT_FOUND; throw new ProxyException(HttpStatus.SC_NOT_FOUND,"There was an error connecting to the target resource! The target url is "+this.getRequestBean(),e); }catch(SocketTimeoutException e){ statusCode=HttpStatus.SC_REQUEST_TIMEOUT; throw new ProxyException(HttpStatus.SC_REQUEST_TIMEOUT,"There was a timeout error connecting to the target resource! The target url is "+this.getRequestBean(),e); }catch(SSLHandshakeException e){ statusCode=HttpStatus.SC_BAD_GATEWAY; throw new ProxyException(HttpStatus.SC_BAD_GATEWAY,"proxy_ui_ssl_certificate_not_trusted! The target url is "+this.getRequestBean(),e); }finally{ long after=System.currentTimeMillis(); long executeTime=after-before; //如果执行时间过长,记录警告日志 if(statusCode=ProxyConstants.PROXY_INVOKE_TIME_WARN_THRESHOLD){ log.info("The proxy cost ms to execute,it's too long."+this.getRequestBean()); } //如果不是401和403的错误码,记录警告日志 if(statusCode>=400 && statusCode!=401 && statusCode!=403){ log.info("Faild to execute the proxy,http status code is "+statusCode+" "+this.getRequestBean()); } //保存http返回码到上下文中 this.setAttribute(ProxyConstants.RESPONSE_STATUS_CODE, new Integer(statusCode)); //将返回码结果保存在临时InputStream中,方便其它过滤器进行处理 //在返回304的时候,Stream为null if(httpMethod.getResponseBodyAsStream()!=null){ this.setResultStream(new ByteArrayInputStream(IOUtils.toByteArray(httpMethod.getResponseBodyAsStream()))); } } }
后置逻辑
protected void doAfterProxy() throws ProxyException,IOException{ //1.content-Type和gizp String mappingPath=request.getPathInfo(); ServiceModel serviceModel=EhCache.getInstance(Constant.EHCACHE).get(Constant.PROXY_SERVICE, mappingPath); if(serviceModel.getGzip().endsWith(Constant.GZIPY)){ this.setAttribute(ProxyConstants.RESPONSE_GZIP_FLAG, Boolean.TRUE); } if(serviceModel.getContentType()!=null){ this.setAttribute(ProxyConstants.RESPONSE_CONTENT_TYPE, serviceModel.getContentType()); } //2.处理来自http client的response header Header [] headers=httpMethod.getResponseHeaders(); for(int i=0;i<headers.length;i++){ Header header=headers[i]; if("Content-Type".equalsIgnoreCase(header.getName()) || "Transfer-Encoding".equalsIgnoreCase(header.getName())){ continue; } this.addResponseHeader(header.getName(), header.getValue()); } }
三 ProxyFilterChainFactory.java
proxy filter chain的工厂类,负责实例化ProxyFilterChain
public ProxyFilterChain create(ListcustomFilters){ Listfilters =new ArrayList(); //添加系统级的前置过滤器 this.addProxyFilter(filters, ProxyInitFilter.class); this.addProxyFilter(filters, ValidationFilter.class); this.addProxyFilter(filters, BrowserCacheFilter.class); this.addProxyFilter(filters, AppCacheFilter.class); //添加用户级的自定义过滤器 if(customFilters!=null){ filters.addAll(customFilters); } //添加系统级的后置过滤器 this.addProxyFilter(filters, ProxyExecuteFilter.class); return new ProxyFilterChainImpl(filters); } private void addProxyFilter(Listfilters,Class clazz){ try{ ProxyFilter filter =this.getFilter(clazz.getCanonicalName()); if(filter==null){ filter=(ProxyFilter) clazz.newInstance(); //只有线程安全(单例)的proxyFilter才放到filter池中 if(filter.isSingleton()){ filterMap.put(clazz.getCanonicalName(), filter); } } filters.add(filter); }catch(IllegalAccessException e){ }catch(InstantiationException e){ } }
四 ProxyInitFilter.java
public class ProxyInitFilter extends AbstractProxyFilter{ public void doFilter(ProxyBean proxy, ProxyFilterChain filterChain) throws ProxyException, IOException { HttpServletRequest request=proxy.getRequest(); filterChain.doFilter(proxy); //设置response content-Type String contentType=(String)proxy.getAttribute(ProxyConstants.RESPONSE_CONTENT_TYPE); if(contentType!=null){ proxy.getResponse().setContentType(contentType); } //设置http返回码 Integer statusCode=(Integer)proxy.getAttribute(ProxyConstants.RESPONSE_STATUS_CODE); if(statusCode!=null){ proxy.getResponse().setStatus(statusCode); if(proxy.getResultStream()!=null){ IOUtils.copy(proxy.getResultStream(),proxy.getResponse().getOutputStream()); }else{ throw new ProxyException(HttpStatus.SC_INTERNAL_SERVER_ERROR,"Unable to get proxy http response status code."); } } } public boolean isSingleton(){ return true; } }
五 ValidationFilter.java
public class ValidationFilter extends AbstractProxyFilter{ public void doFilter(ProxyBean proxy, ProxyFilterChain filterChain) throws ProxyException, IOException { this.validateRequest(proxy); filterChain.doFilter(proxy); this.validateResponse(proxy); } private void validateRequest(ProxyBean proxy) throws ProxyException{ HttpServletRequest request=proxy.getRequest(); ServiceModel serviceModel=EhCache.getInstance(Constant.EHCACHE).get(Constant.PROXY_SERVICE, request.getPathInfo()); String rquestType=serviceModel.getMethod(); String [] rqs=rquestType.split("\,"); ListreqList=new ArrayList(); for(int i=0;i<rqs.length;i++){ reqList.add(rqs[i]); } if(!reqList.contains(request.getMethod().toLowerCase())){ throw new ProxyException(HttpStatus.SC_FORBIDDEN,request.getMethod()+" method for proxy uri "+proxy.getRequestBean().toString()+" is not allowed"); } } private void validateResponse(ProxyBean proxy) throws ProxyException, IOException{ //判断返回到mime-type是否合法 HttpServletResponse response=proxy.getResponse(); String contentType=(String)proxy.getAttribute(ProxyConstants.RESPONSE_CONTENT_TYPE); if(contentType!=null){ if(true){ response.sendError(HttpStatus.SC_FORBIDDEN,"The response MIME-TYPE is not allowed"); return; } } } public boolean isSingleton(){ return true; } }
六 BrowserCacheFilter.java
浏览器缓存
public class BrowserCacheFilter extends AbstractProxyFilter{ public void doFilter(ProxyBean proxy, ProxyFilterChain filterChain) throws ProxyException, IOException { filterChain.doFilter(proxy); Integer statusCode=(Integer)proxy.getAttribute(ProxyConstants.RESPONSE_STATUS_CODE); if((statusCode!=null && statusCode==HttpStatus.SC_OK) && (proxy instanceof GetProxy)){ try{ //增加缓存设置 int broswer_inteval=3000; CacheControl cc=new CacheControl(); cc.setMaxAge(broswer_inteval); proxy.getResponse().setHeader("Cache-Control", cc.toString()); long expires=System.currentTimeMillis()+broswer_inteval*1000; proxy.getResponse().setDateHeader("Expires", expires); }catch(Exception ex){ //出错不进行任何处理,不抛异常 } } } public boolean isSingleton(){ return true; } }
七 AppCacheFilter.java
应用层缓存,其中应用层缓存可以根据需要做一级缓存(本地缓存)和二级缓存(分布式缓存)
public void doFilter(ProxyBean proxy, ProxyFilterChain filterChain) throws ProxyException, IOException { HttpServletRequest request=proxy.getRequest(); Boolean isCache=false; //只有GET的情况下,才使用缓存 if((proxy instanceof GetProxy) && isCache){ proxy.setAttribute(ProxyConstants.APP_CACHE_ENABLE_KEY, Boolean.TRUE); String key=proxy.getRequestBean().toString();//应用缓存的key指,采用proxy的URL //判断一级缓存 CachedResponse cachedResponse=EhCache.getInstance(Constant.EHCACHE).get("proxy_response", key); if(cachedResponse==null){ filterChain.doFilter(proxy); //只有返回码是200(成功),才将response的值保存到缓存中 Integer statusCode=(Integer)proxy.getAttribute(ProxyConstants.RESPONSE_STATUS_CODE); if(statusCode!=null && statusCode==HttpStatus.SC_OK){ ListrespHeaders=(List)proxy.getAttribute(ProxyConstants.RESPONSE_KEADERS_KEY); String contentType=(String)proxy.getAttribute(ProxyConstants.RESPONSE_CONTENT_TYPE); cachedResponse=new CachedResponse(proxy.getResultStream(),respHeaders,contentType); //由于resultStream被copy一次,因此重新初始化到proxy bean中 proxy.setResultStream(cachedResponse.getRespInputStream()); EhCache.getInstance(Constant.EHCACHE).set("proxy_response", key, cachedResponse); } }else{ proxy.setResultStream(cachedResponse.getRespInputStream()); proxy.setAttribute(ProxyConstants.RESPONSE_CONTENT_TYPE, cachedResponse.getContentType()); //设置http response header if(CollectionUtils.isNotEmpty(cachedResponse.getRespHeaders())){ for(NameValuePair header:cachedResponse.getRespHeaders()){ proxy.getResponse().setHeader(header.getName(), header.getValue()); } } //由于没有真正调用proxy http请求,因此认为此请求是成功的 proxy.setAttribute(ProxyConstants.RESPONSE_STATUS_CODE, new Integer(HttpStatus.SC_OK)); } }else{ filterChain.doFilter(proxy); } }
八 ProxyExecuteFilter.java
public class ProxyExecuteFilter extends AbstractProxyFilter{ public void doFilter(ProxyBean proxy, ProxyFilterChain filterChain) throws ProxyException, IOException { proxy.doProxy(); } public boolean isSingleton(){ return true; } }
调用的就是ProxyBean.java里面的实现
public final void doProxy() throws ProxyException,IOException{ this.doBeforeProxy(); this.doExecute(); this.doAfterProxy(); }
9后台演示地址
账号和密码在QQ群。
QQ群交流:124020918
10源码下载地址
正在整理,回头开放。
QQ群下载:124020918


原创不易,谢谢赞赏。你的支持就是我的动力,我会更加努力。
不知道源码博主什么时候可以放出来。