统一接口平台(有演示地址)

2018-12-03 21:34:35

1产品介绍

前端应用系统通过统一接口平台获取数据,不直接与外部系统接口打交道。统一接口平台通过多种方式与外部系统联接获取数据并向各前端应用系统提供各种数据格式包,将外部系统有效地隔离在业务系统之外。前端应用系统需要请求的外部接口需要在统一接口平台注册,开放。每次访问都会被有效的记录,实行监管。

2应用范围

前后端分离,跨域,缓存策略,接口管控,服务组装,负载均衡等。

3技术描述

应用包:servlet,ehcache,memcached/redis,http协议,http连接池,apache下httpclient,IOUtils,装饰模式,责任链模式,享元模式。

后台包:Spring MVC,Mybatis。

4架构描述

interface1.png

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文件格式如下: 

xml.png

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后台演示地址

http://proxyadmin.51jdk.com

账号和密码在QQ群。

QQ群交流:124020918

10源码下载地址

正在整理,回头开放。

QQ群下载:124020918

(微信打赏)

(支付宝打赏)
原创不易,谢谢赞赏。你的支持就是我的动力,我会更加努力。

当今主流云服务器代金劵,优惠折扣最大力度推荐。

阿里云产品1888元服务器代金券大礼包免费领取。

阿里云服务器低门槛上云捷径,普惠上云,云服务器1核1G仅需293元/年。

企业级高性能实例,限时2-5折,限首次购买ECS用户参与,限购4台。

腾讯云代金券新用户满200减150 满500减375 满1000减750 满2000减1500。

腾讯云服务器,数据库热卖云产品3折起,爆品低至1折。

腾讯云业界领先的性价比,0.57元/天起,关注最新活动,免费体验产品。