随着前后端分离的大热,WebApi在项目中的作用也是越来越重要,由于公司的原因我之前一直没有机会参与前后端分离的项目,但WebApi还是要学的呀,因为这东西确实很有用,可单独部署、与前端和App交互都很方便,既然有良好的发展趋势,我们当然应该顺势而为——搞懂WebApi!

从MVC到WebApi,路由机制一直都在其中扮演着重要的角色。

它可以很简单:如果你只需要会用一些简单的路由,如/Home/Index那么你只需要配置一个默认路由就能搞定。

它可以很神秘:你的url可以千变万化,看到一些“无厘头”的url,很难理解它是如何找到匹配的Action,例如/api/Pleasure/1/detail,这样的url可以让你纠结半天。

它可以很深奥:当面试官提问“请简单分析下MVC路由机制的原理”,你可能事先就准备好了答案,然后劈里啪啦一顿(型如:UrlRoutingMoudle—>Routes—>RouteData—>RequestContext—>Controller),你可能回答的很流利,但并不一定理解这些个对象到底是啥意思。):目前为止我还没能理解透,以后会继续努力的直到弄清楚。

一、MVC和WebApi路由机制比较
1、MVC使用的路由
在MVC中,默认路由机制是通过解析url路径来匹配Action。比如:/User/GetList,这个url就表示匹配User控制器下的GetList方法,这是MVC路由的默认解析方式。为什么默认的解析方式是这样子的呢?因为MVC定义了一个默认路由,路由代码放在App_Start文件夹下的RouteConfig.cs中,今后我们如果想要自定义路由规则,那自定义路由的代码也要写在RouteConfig.cs中。

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
url:"{controller}/{action}/{id}"定义了路由解析规则,{controller}是必填参数默认值是Home,{action}是必填参数默认值是Index,{id}表示匹配名称为id的形参,而且是可选参数(方法的参数列表中可以有名为id的形参,也可以没有)。

2、WebApi使用的路由
在WebApi中,默认路由机制是通过解析http请求的类型来匹配Action,也就是说WebApi的默认路由机制不需要指定Action的名称。比如:/api/Pleasure,这个url就表示匹配Pleasure控制器下的[HttpGet]方法,/api是固定必填值,这是WebApi路由的默认解析方式。WebApi的默认解析方式之所以如此,同样也是因为定义了默认路由,路由代码放在App_Start文件夹下的WebApiConfig.cs中,今后我们如果想要自定义路由规则,那自定义路由的代码也要写在WebApiConfig.cs中。

public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

}
routeTemplate:"api/{controller}/{id}"定义了路由解析规则,api是固定必填值,{controller}是必填参数无默认值,{id}表示匹配名称为id的形参,而且是可选参数(方法的参数列表中可以有名为id的形参,也可以没有)。

3、MVC和WebApi路由区别汇总
WebApi的默认路由机制通过http请求的类型匹配Action,MVC的默认路由机制通过url匹配Action
WebApi的路由配置文件是WebApiConfig.cs,MVC的路由配置文件是RouteConfig.cs
WebApi的Controller继承自Web.Http.ApiController,MVC的Controller继承自Web.Mvc.Controller
4、示例一
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}
}
 
为什么http://localhost:7866/api/Pleasure能匹配到GetOne()方法呢?首先根据路由规则解析出控制器是Pleasure,其次通过浏览器地址栏直接发出的请求都是get请求而Pleasure中只有一个get类型的方法,因此就匹配到了GetOne()。

为什么http://localhost:7866/api/Pleasure/1也能匹配到GetOne()方法呢?因为id是可选形参,即使指定了id的值,也可以访问不含形参id的方法。

5、示例二
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含参-value1", "含参-value2" };
}
}

示例二中http://localhost:7866/api/Pleasure请求的结果与示例一中的结果是一样的,在此不做过多的解释。

示例二中http://localhost:7866/api/Pleasure/1请求到了含参方法,说明如果指定了形参id的值,而且Controller中存在指定请求类型的含参方法,会优先匹配含此形参的方法。若匹配不上含参方法,但参数类型为可选参数,那就再尝试匹配不含此形参的方法。

6、示例三
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含参-value1", "含参-value2" };
}

[HttpGet]
public string GetList()
{
return "value";
}
}

虽然WebApi的默认路由机制不需要指定Action,但是WebApi支持在url中指定Action。由于Restful风格的服务要求请求的url中不能包含Action,因此WebApi不提倡在url中指定Action。

7、示例四
public class PleasureController : ApiController
{
[HttpGet]
public string GetOne(int id,string name,int age)
{
return string.Format("id={0},name={1},age={2}", id, name, age);
}
}

二、WebApi基础
1、默认路由
新建WebApi服务的时候,会自动在WebApiConfig.cs文件里面生成一个默认路由:

public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
将MapHttpRoute()方法转到定义可以发现,它有四个重载方法:

下面来看看各个参数的作用:

name:"DefaultApi"表示此路由的名称。注册多个路由时必须保证名称不重复。
routeTemplate:"api/{controller}/{id}"表示路由的匹配规则。api是固定必填值,这个值是可变的如果你把它改成“BalaApi”,那url就应该变成 “http://1.1.1.1:80/BalaApi/***”。{controller}是控制器的占位符,在真实的url中,该部分对应的是具体的控制器名称,这个和MVC一致。{id}是形参id的占位符,id是形参的名字,一般这个参数都会在defaults中设置为可选。
defaults:new { id=RouteParameter.Optional }表示设置id为可选参数,routeTemplate中的{controller}和{id}都可以设置默认值。若defaults改成new { controller="Pleasure", id = RouteParameter.Optional },那么我们请求http://1.1.1.1:80/api这个url仍然能访问到Pleasure控制器中的[HttpGet]方法。
constraints:new{ id = @"\d+" }表示为id添加约束。形参id不能为空而且必须是整数,优先匹配含参方法,也能匹配无参方法(详情请回看上一部分的示例一)。
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含参-value1", "含参-value2" };
}
}

我们如果想在不指定id值的时候仍然能够正常请求到GetOne(),把id的约束改成“constraints: new { id = @"\d*" }”即可,这里就不放截图了,大家可以自己去试试。我们只在路由中对id的值进行了约束,其实我们也可以约束Controller和Action,但一般不常用大家有兴趣可以自己玩一下。

2、自定义路由
上面介绍了许多关于默认路由的内容,除此之外我们还可以自定义路由,比如将WebApiConfig.cs改成下面这样:

public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
2.1、自定义匹配到Action的路由
这个自定义路由很好理解,它和MVC中的默认路由大致相同,不同之处是此路由多了个api前缀。
添加了可匹配到Action的路由后,就能解决“第一部分->示例三”中遇到的问题了。
当同时存在多个路由时,一定要注意name不能重复。

public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "GetOne(int id)->value1", "GetOne(int id)->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

我艹,为什么http://localhost:7866/api/Pleasure/GetOne请求失败了,为什么没有匹配上GetOne()方法?
下面我来解释下,DefaultApi定义在ActionApi之前,所以先检查http://localhost:7866/api/Pleasure/GetOne能否与DefaultApi匹配成功,api是固定必填值,Pleasure是controller的值,GetOne是id的值,匹配成功!然后去PleasureController中找[HttpGet]类型而且含有string id形参的方法,当然是没有啦。只找到一个[HttpGet]类型的GetOne(int id),所以错误信息中提示参数id的类型不匹配。

接下来说说解决方法,实际上id是int类型的参数,我们希望GetOne能匹配上ActionApi中的action,而不是DefaultApi中的id,这在默认情况下是做不到的,不过为DefaultApi中的id加上个约束后就能做到了,路由修改后如下:

public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d*" }
);

config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

2.2、修改方法的ActionName
WebApi默认GetOne()方法的ActionName就是GetOne,如果我们想要方法名和ActionName不一致,可以通过ActionName特性来修改。

public class PleasureController : ApiController
{
[ActionName("NewGetOne")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "GetOne(int id)->value1", "GetOne(int id)->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

三、WebApi路由执行过程
1、路由原理
有了上面的理论作为基础,我们再来分析WebApi路由机制的原理以及路由匹配的过程。由于WebApi的路由机制和MVC有许多相似性,所以要想理解WebApi的路由机制,就需要搬出那些Asp.Net Routing里面的对象。这个过程有点复杂,我就根据搜罗的资料和自己的理解 ,提一些主要的过程:

a、WebApi服务启动之后,会执行全局配置文件Global.asax.cs的 protected void Application_Start() 方法,其中的 GlobalConfiguration.Configure(WebApiConfig.Register); 会通过委托的方式执行WebApiConfig.cs中的 public static void Register(HttpConfiguration config),将配置的所有路由信息添加到HttpRouteCollection对象中保存起来。这里的HttpRouteCollection对象的实例名是Routes,这个很重要,后面会用到。

b、当我们发送请求到WebApi服务器的时候,比如当我们访问http://localhost:7866/api/Pleasure时,首先请求会被UrlRoutingModule监听组件截获,然后按照定义顺序检查url能匹配上Routes集合中的哪个路由模板(如果都匹配不上,则返回404),最后返回对应的IHttpRoute对象。IHttpRoute对象是url匹配上的Routes集合里面的一个实体。

c、将IHttpRoute对象交给当前的请求上下文对象RequestContext处理,根据IHttpRoute对象中的url匹配到对应的controller,然后根据http请求的类型和参数找到对应的action。这样就能找到请求对应的controller和action了。

这个过程是非常复杂的,为了简化,我只选择了最主要的几个过程。更详细的路由机制可以参考http://www.cnblogs.com/wangiqngpei557/p/3379095.html,这篇文章写得有些深度,感兴趣的朋友可以看下。

2、根据请求的url匹配路由模板
通过上文路由过程的分析,我们知道一个请求过来之后,路由主要经历三个阶段:
1、根据请求的url匹配路由模板
2、找到controller
3、找到action

第一步很简单,主要就是将url与路由模板中配置的routeTemplate进行匹配校验,在此不做过多的说明。

3、找到controller
你如果反编译路由模块的代码,就会发现控制器的选择主要在IHttpControllerSelector接口中的SelectController()方法里面处理。

该方法所需的HttpRequestMessag参数,是由当前请求封装而来,返回HttpControllerDescriptor对象。这个接口默认由DefaultHttpControllerSelector类提供实现。

默认实现的方法里面大致的算法机制是:首先在路由字典中找到实际的控制器名称(比如“Pleasure”),然后在此控制器名称后面加上“Controller”字符串得到控制器的全称“PleasureController”,最后找到WebApi对应的Controller再实例化就得到当前请求的控制器对象。

4、找到action
得到了控制器对象之后,Api引擎通过调用IHttpActionSelector接口中的SelectAction()方法去匹配action。这个过程主要包括:

解析当前http请求,得到请求类型(post、delete、put、get)
如果路由模板配置了{action},则直接去url中找action的名称
解析请求的参数
如果路由模板配置了{action},那么直接去url中找对应的action即可。如果没有配置action,则会首先匹配请求类型(post/delete/put/get),然后匹配请求参数,才能找到对应的action。

5、设置方法同时支持多种http请求类型
WebApi提供了AcceptVerbs,应用这一特性可以使方法同时支持多种请求类型。这一特性在实际中使用不是太多,大家了解即可。

[AcceptVerbs("get","post")]
public string GetList()
{
return "GetList()->value";
}
四、WebApi特性路由
如果http请求的类型相同(比如都是get请求),并且请求的参数也相同。这个时候似乎就有点不太好办了,这种情况在实际项目中还是比较多的,比如:

public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
 当然这个问题可以通过添加匹配到Action的路由来解决,不过这就不符合Restful风格了,所有我们不提倡这种写法。除此之外,利用特性路由也能解决上述问题,下面说说特性路由的东西。

1、启动特性路由
public static void Register(HttpConfiguration config)
{
//启动特性路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d*" }
);

config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
一般情况下通过新版本的VS2017创建WebApi项目时,这句话默认已经存在。

2、最简单的无参特性路由

2.1、为GetOne()方法添加Route特性(特性路由没参数,GetOne()方法也没参数)
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

结论:

为GetOne()添加了特性路由后,无法通过WebApi默认路由机制(不指定Action)访问GetOne()
为GetOne()添加了特性路由后,无法通过指定ActionName的方式访问GetOne()
为GetOne()添加了特性路由后,只能通过特性路由的路径访问GetOne()
2.2、为GetOne()方法添加Route特性(特性路由没参数,GetOne()方法有参数)
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(string name)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

特性路由不含参数时的结论:

方法没参数时,只要url满足特性路由的定义就能访问到方法
方法有参数时,必须url满足特性路由的规则而且指定了方法所需的参数值才能访问到方法
3、含参特性路由
3.1、特性路由有参数,方法没参数
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

3.2、特性路由有参数,方法有参数
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne(string name)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne(string address)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

特性路由含参数时的结论:

方法没参数时,只要url满足特性路由的定义就能访问到方法
方法有参数时,必须url满足特性路由的规则而且指定了方法所需的参数值才能访问到方法
3.3、特性路由有参数,参数在最后而且有默认值
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{age:int=16}")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

3.4、特性路由有参数,参数在中间而且有默认值
public class PleasureController : ApiController
{
[Route("Pleasure/{age:int=16}/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

4、路由前缀
在正式项目中,同一个控制器中所有的action的特性路由最好有一个相同的前缀,这并非是必须的,但能增加url的可读性。一般的做法是在控制器上使用[RoutePrefix]特性来标识。当我们使用特性路由来访问时,前边必须加上定义的前缀。

[RoutePrefix("api/prefix")]
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{age:int=16}")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}

[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}

————————————————
版权声明:本文为CSDN博主「changuncle」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaouncle/article/details/83869952

转载于:https://www.cnblogs.com/dongh/p/11387454.html

WebApi路由机制详解相关推荐

  1. C#WebApi路由机制详解

    随着前后端分离的大热,WebApi在项目中的作用也是越来越重要,可单独部署.与前端和App交互都很方便,既然有良好的发展趋势,我们当然应该顺势而为--搞懂WebApi!Restful相当于给Http请 ...

  2. WebApi路由机制详解——看完不会用你打我

    随着前后端分离的大热,WebApi在项目中的作用也是越来越重要,由于公司的原因我之前一直没有机会参与前后端分离的项目,但WebApi还是要学的呀,因为这东西确实很有用,可单独部署.与前端和App交互都 ...

  3. WebApi 路由机制剖析

    阅读目录 一.MVC和WebApi路由机制比较 1.MVC里面的路由 2.WebApi里面的路由 二.WebApi路由基础 1.默认路由 2.自定义路由 3.路由原理 三.WebApi路由过程 1.根 ...

  4. php 反射原理,PHP反射机制详解

    本文主要和大家分享PHP反射机制详解,内容包括1.自动生成文档2.实现 MVC 架构3.实现单元测试4.配合 DI 容器解决依赖,希望能帮助到大家. 1.自动生成文档 根据反射的分析类,接口,函数和方 ...

  5. Dubbo 原理和机制详解

    Dubbo 是一款Java RPC框架,致力于提供高性能的 RPC 远程服务调用方案.作为主流的微服务框架之一,Dubbo 为开发人员带来了非常多的便利. 1. Dubbo核心功能 Dubbo主要提供 ...

  6. HTTP缓存机制详解

    HTTP缓存机制详解 一. 前言 二. 缓存的介绍 什么是缓存? 为什么要使用缓存? 1. 减少冗余的数据传输 2. 缓解带宽瓶颈 3. 破坏瞬间拥塞 4. 降低距离时延 三. 缓存有效性 命中和未命 ...

  7. PHP autoload机制详解

    PHP autoload机制详解 转载自 jeakccc PHP autoload机制详解 (1) autoload机制概述 在使用PHP的OO模式开发系统时,通常大家习惯上将每个类的实现都存放在一个 ...

  8. 模糊匹配 读音_onenote搜索机制详解②:两种搜索模式,模糊与精确匹配

    先从纯文本搜索讲起,这是最基本也是最重要的. 从这篇开始,以及接下来连续几篇文章,都会介绍搜索的基础功能.注意,这几篇文章中谈论的都是基本的.正常的搜索功能,暂时不考虑Bug等因素. 在很多软件(例如 ...

  9. Java类加载机制详解【java面试题】

    Java类加载机制详解[java面试题] (1)问题分析: Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数 ...

最新文章

  1. 电脑硬件知识学习_DIY电脑多年,这些硬件冷知识你们知道吗?
  2. #1403 : 后缀数组一·重复旋律 (可重叠最长重复K次子串问题)
  3. OptaPlanner - 把example运行起来(运行并浅析Cloud balancing)
  4. MFCk开发IM-msvcrt.lib 和 libcmtd.lib 冲突的解决方案
  5. Facebook将用实际行动引导青少年远离有害内容
  6. Warning: lio_listio returned EAGAIN Performance degradation may be seen
  7. 【NCRE】2020年9月全国计算机等级考试,计算机二级MS Office高级应用划水贴
  8. 让 ASP.NET AJAX 支持浏览器的 History Navigation - Part 1
  9. systemctl/systemd 常用命令
  10. apache 的batik生成svg文件和通过swing界面查看效果
  11. JSON网络传输协议
  12. 2021蓝桥杯Java复习【史上最详细攻略】【持续更新】
  13. Android混淆介绍
  14. 单词记忆系统-项目需求分析
  15. 安卓桌面软件_Windows启动器v8.34安卓用上win桌面
  16. 朱慕慕:ui设计包括什么内容,ui设计包括有几部分内容?
  17. 在word中输入文字后面的文字会被删除?原来是这么回事!
  18. (0105)iOS开发之iOS13 暗黑模式(Dark Mode)适配
  19. PCI/PCIe 的那些事(1)- 总线基本知识
  20. 昆仑通态触摸屏的使用

热门文章

  1. antd 给input设置值_AntD Input onChange 中文输入法
  2. 超市信息管理程序c语言购物车,c语言购物车代码
  3. LOL进游戏,游戏界面全黑(不是进入峡谷)解决办法(亲测)
  4. IP数据报的检验(计算机网络)
  5. chinapay java_Chinapay应用技术部Java编码规范.docx
  6. Oracle数据库实例之内存架构(一)
  7. 利用PaddleOCR训练车牌识别模型
  8. 国内镜像下载python文件
  9. 匹兹堡大学计算机系世界排名,匹兹堡大学计算机排名
  10. ebs 系统 登录 java_Oracle EBS登录页面显示空白