统一接口平台

article/2025/9/22 22:01:32

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 filterListcustomFilters=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中系统保留的paramif(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 headerEnumerationem=request.getHeaderNames();while(em.hasMoreElements()){String headerName=em.nextElement();String headerValue=request.getHeader(headerName);this.httpMethod.addRequestHeader(headerName,headerValue);}//2.处理http cookiesif(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为nullif(httpMethod.getResponseBodyAsStream()!=null){this.setResultStream(new ByteArrayInputStream(IOUtils.toByteArray(httpMethod.getResponseBodyAsStream())));}}}

后置逻辑

protected void doAfterProxy() throws ProxyException,IOException{				//1.content-Type和gizpString 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 headerHeader [] 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-TypeString 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 headerif(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


http://chatgpt.dhexx.cn/article/UxSeabwN.shtml

相关文章

接口开放平台,我的一些思考

1. 概述 接口开放平台&#xff0c;为访问者提供统一的URL作为唯一访问入口&#xff0c;参数中指定具体接口标识和接口参数。它的优点包括接口规范性、效率、管理能力。接口开放平台可以分为简单的和复杂的两种方式。简单的接口开放平台&#xff0c;由自身提供接口实现。复杂的…

通用接口开放平台设计与实现——(1)开篇

首先简单聊下背景&#xff0c;大概是几年前&#xff0c;工作中遇到这么一个复杂的应用场景&#xff0c;大型企业中的物流系统&#xff0c;需要跟众多的内外部系统交互&#xff0c;对接的系统达到十几个&#xff0c;接口数量在50个左右&#xff0c;并且后续还会大量增加新的对接…

六大接口管理平台,总有一款适合你的!

前后端分离绕不开的接口测试 先聊一聊前端和后端分离的优点。前后端分离优点如下&#xff1a; 真正的实现前后端解耦&#xff0c;前端服务器使用 nginx&#xff1b;易于发现bug&#xff0c;可以快速定位是谁的问题&#xff0c;不会出现互相踢皮球的现象&#xff1b;减少后端服…

2022中国十大API接口服务平台排行榜(推荐五颗星)

Api接口也就是所谓的应用程序接口&#xff0c;api接口的全称是Application Program Interface&#xff0c;通过API接口可以实现计算机软件之间的相互通信&#xff0c;开发人员可以通过API接口程序开发应用程序&#xff0c;可以减少编写无用程序&#xff0c;减轻编程任务&#x…

python基础教程:startswith()和endswith()的用法

startswith()方法 Python startswith() 方法用于检查字符串是否是以指定子字符串开头如果是则返回 True&#xff0c;否则返回 False。如果参数 beg 和 end 指定值&#xff0c;则在指定范围内检查。str.startswith(str, beg0,endlen(string)); 参数 str --检测的字符串。strbeg …

C# 为什么要用StartsWith而不是Substring进行字符串匹配?

字符串的截取匹配操作在开发中非常常见&#xff0c;比如下面这个示例&#xff1a;我要匹配查找出来字符串数组中以“abc”开头的字符串并打印&#xff0c;我下面分别用了两种方式实现&#xff0c;代码如下&#xff1a; using System;namespace ConsoleApp23 {class Program{sta…

C# 为什么高手喜欢用StartsWith而不是Substring进行字符串匹配?

字符串的截取匹配操作在开发中非常常见&#xff0c;比如下面这个示例&#xff1a;我要匹配查找出来字符串数组中以“abc”开头的字符串并打印&#xff0c;我下面分别用了两种方式实现&#xff0c;代码如下&#xff1a; using System;namespace ConsoleApp23 {class Program{sta…

startsWith(),endsWith()的作用和用法

startsWith()&#xff0c;endsWith()的作用&#xff0c;用法&#xff0c;判断字符串a是不是以字符串b开头或结尾 Java代码 if(a.startsWith(b)) //判断字符串a 是不是以字符串b开头. Java代码 if(a.endsWith(b)) //判断字符串a 是不是以字符串b结尾. 一个demo测试 Java…

[PYTHON] python中startswith函数用法

一、当不确定python中某一函数是做什么用的可以进入函数里面分析它的源码 大致意思就是&#xff1a;如果prefix在S中以start开头&#xff0c;以end结尾&#xff0c;返回结果为True&#xff0c;否则返回False 由上得知&#xff0c;其语法如下&#xff1a; S.startswith(prefix[,…

推荐两个漂亮的编程字体

【摘要】推荐两个漂亮的编程字体1. Inconsolata号称最好看的编程字体。2. YaHei Consolas Hybrid中英文混合&#xff0c;是微软专为编程做制作的字体&#xff0c;很优雅很强大。 1. Inconsolata 号称最好看的编程字体。在个别编辑器下中文的显示会有bug&#xff08;比如笔者用…

编程常用字体Consolas、Fira Code、Source Code Pro

编程时方便查看&#xff0c;且中文符号与英文符号区别大&#xff0c;便于排错 1、Consolas 一种等宽字体、属于最常用的编码字体下载地址 参考图&#xff1a; 2、Fira Code 一种专为代码排版优化的等宽字体&#xff0c;基于 Fira Mono 等宽字体的一个扩展&#xff0c;主要特点…

10个不错的编程等宽字体

1. Source Code Pro Adobe 在发布首款开源字体 Source Sans Pro 后&#xff0c;2012年9月又发布了 Source Code Pro 字体集&#xff0c;它是一款非常适合程序员阅读的等宽字体&#xff0c;适用于 Linux/Unix、Mac OS X 和 Windows 系统&#xff0c;由 Paul D. Hunt 设计&…

学编程必备:3 大编程字体!让你在编程的时候原地起飞!

前言 日复一日的编写代码&#xff0c;有没有感到审美疲劳&#xff1f; 对于编码的我们&#xff0c;一个赏心悦目的字体&#xff0c;可以愉悦身心。 编程字体&#xff0c;需要的是审美感不疲劳&#xff0c;更主要的是能显示出清晰的字体效果。 比如 [‘i’, ‘1’, ‘l’] 与…

适合matlab的编程字体“YAHEI CONSOLAS HYBRID”-下载+安装

“适合matlab的编程字体“YAHEI CONSOLAS HYBRID”-下载安装 前言下载YAHEI CONSOLAS HYBRID安装步骤在MATLAB安装该字体 前言 matlab预设的字体及字号可能不符合大多数国人的使用习惯&#xff0c;YAHEI CONSOLAS HYBRID字体是一款无衬线字体&#xff0c;是很适合程序员使用的…

vscode编程字体设置与修改

vscode编程字体设置与修改 在vocode上写代码&#xff0c;不会向平常一样写出如下符号 这里介绍一下怎么设置&#xff0c;并且使用vocode官方推荐的字体FiraCode 首先 进入https://github.com/tonsky/FiraCode下载文件包&#xff0c;下载到本地后&#xff0c;进入里面的ttf文…

QT设置编程字体

现在记录一下自己思路或者一些常用的知识点&#xff0c;这样以后自己忘记了可以很快查找翻阅一下&#xff0c;可以当个备忘录。同时如果这些东西对其他人有帮助的话&#xff0c;也是特别好的&#xff0c;也会让自己有成就感&#xff0c;给自己更多的前进动力。 这里搜索了很多资…

c语言编程用什么字体,10 款最适合编程的字体

编程需要什么样的字体&#xff1f;什么样的字体适合编程&#xff1f;首先字体是要免费的&#xff01;其次这种字体能够轻松的区分0O并且1lI&#xff0c;避免有用这些字符的混淆而引起程序的bug。还要字体要好看&#xff0c;容易阅读等。 1. Hack Hack 是基于 Bitstream Vera 和…

10大最适合编程的字体,让代码看起来更美更舒服!

现在有事没事就喜欢写写代码的人越来越多了&#xff0c;对于成天盯着屏幕工作的开发人员来说&#xff0c;编程代码可能是每天见得最多的东西了。可是绝大部分人都一直使用编辑器默认的字体&#xff0c;其实&#xff0c;换一套适合自己的编程字体不仅能让代码看得更舒服&#xf…

编程字体推荐

前言 对于编码的我们&#xff0c;一个赏心悦目的字体&#xff0c;可以愉悦身心。 以下是我推荐一波等宽字体&#xff0c;有兴趣的可以装一波。 字体(不分先后) Fira Code 这个字体我用了四年&#xff0c;编码仪式感满满的(逼格)。 效果图 下载 Github:FiraCode Cascadia Co…

分享一些我常用的编程字体

1.前言 可能有的同学已经放假在家了&#xff0c;我也快了。最近不想聊具体的技术了&#xff0c;过年了聊点轻松的东西。但是又不想偏离编程太远&#xff0c;所以我今天聊聊我们常用的一些编程字体。在阅读代码的时候眼睛需要以不同寻常的方式移动&#xff08;垂直和水平方向&a…