通常我们在程序中需要调用WebService时,都是通过“添加Web引用”,让VS.NET环境来为我们生成服务代理,然后调用对应的Web服务。这样是使工作简单了,但是却和提供Web服务的URL、方法名、参数绑定在一起了,这是VS.NET自动为我们生成Web服务代理的限制。如果哪一天发布Web服务的URL改变了,则我们需要重新让VS.NET生成代理,并重新编译。在某些情况下,这可能是不能忍受的,我们需要动态调用WebService的能力。比如我们可以把Web服务的URL保存在配置文件中,这样,当服务URL改变时,只需要修改配置文件就可以了。
说了这么多,实际上我们要实现这样的功能:
public static object InvokeWebService(string url, string methodname, object[] args)
其中,url是Web服务的地址,methodname是要调用服务方法名,args是要调用Web服务所需的参数,返回值就是web服务返回的结果了。 要实现这样的功能,你需要这几个方面的技能:反射、CodeDom、编程使用C#编译器、WebService。在了解这些知识后,就可以容易的实现web服务的动态调用了:
#region InvokeWebService //动态调用web服务 public static object InvokeWebService(string url, string methodname, object[] args) { return WebServiceHelper.InvokeWebService(url ,null ,methodname ,args) ; } public static object InvokeWebService(string url, string classname, string methodname, object[] args) { string @namespace = "EnterpriseServerBase.WebService.DynamicWebCalling" ; if((classname == null) ||(classname == "")) { classname = WebServiceHelper.GetWsClassName(url) ; } try { //获取WSDL WebClient wc = new WebClient(); Stream stream = wc.OpenRead(url+"?WSDL"); ServiceDescription sd = ServiceDescription.Read(stream); ServiceDescriptionImporter sdi = new ServiceDescriptionImporter(); sdi.AddServiceDescription(sd,"",""); CodeNamespace cn = new CodeNamespace(@namespace); //生成客户端代理类代码 CodeCompileUnit ccu = new CodeCompileUnit(); ccu.Namespaces.Add(cn); sdi.Import(cn ,ccu); CodeDomProvider provider = new CSharpCodeProvider();//设定编译参数 CompilerParameters cplist = new CompilerParameters(); cplist.GenerateExecutable = false; cplist.GenerateInMemory = true; cplist.ReferencedAssemblies.Add("System.dll"); cplist.ReferencedAssemblies.Add("System.XML.dll"); cplist.ReferencedAssemblies.Add("System.Web.Services.dll"); cplist.ReferencedAssemblies.Add("System.Data.dll"); //编译代理类 CompilerResults cr = provider.CompileAssemblyFromDom(cplist, ccu); if(true == cr.Errors.HasErrors) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach(System.CodeDom.Compiler.CompilerError ce in cr.Errors) { sb.Append(ce.ToString()); sb.Append(System.Environment.NewLine); } throw new Exception(sb.ToString()); } //生成代理实例,并调用方法 System.Reflection.Assembly assembly = cr.CompiledAssembly; Type t = assembly.GetType(@namespace+"."+classname,true,true); object obj = Activator.CreateInstance(t); System.Reflection.MethodInfo mi = t.GetMethod(methodname); return mi.Invoke(obj,args); } catch(Exception ex) { throw new Exception(ex.InnerException.Message,new Exception(ex.InnerException.StackTrace)); } } private static string GetWsClassName(string wsUrl) { string[] parts = wsUrl.Split('/') ; string[] pps = parts[parts.Length-1].Split('.') ; return pps[0] ; } #endregion
上面的注释已经很好的说明了各代码段的功能,下面给个例子看看,这个例子是通过访问http://www.webservicex.net/globalweather.asmx 服务来获取各大城市的天气状况。
string url = "http://www.webservicex.net/globalweather.asmx" ; string[] args = new string[2] ; args[0] = this.textBox_CityName.Text ; args[1] = "China" ; object result = WebServiceHelper.InvokeWebService(url ,"GetWeather" ,args) ; this.label_Result.Text = result.ToString() ;
上述的例子中,调用web服务使用了两个参数,第一个是城市的名字,第二个是国家的名字,Web服务返回的是XML文档,可以从其中解析出温度、风力等天气情况。
最后说一下,C#虽然仍属于静态语言之列,但是其动态能力也是很强大的,不信,你可以看看Spring.net的AOP实现,这种“无侵入”的AOP实现比通常的.NET声明式AOP实现(一般是通过AOP Attribute)要漂亮的多。
using System; using System.Collections.Generic; using System.Text; using System.Xml; using System.Net; using System.Web.Services.Description; using System.CodeDom; using System.CodeDom.Compiler; using System.Reflection; namespace WindowsServiceWebDefaultHotCity { /// <summary< /// WebService代理类 /// </summary< public class WebServiceAgent { private object agent; private Type agentType; private const string CODE_NAMESPACE = "Beyondbit.WebServiceAgent.Dynamic"; /// <summary< /// 构造函数 /// </summary< /// <param name="url"<</param< public WebServiceAgent(string url) { XmlTextReader reader = new XmlTextReader(url + "?wsdl"); //创建和格式化 WSDL 文档 ServiceDescription sd = ServiceDescription.Read(reader); //创建客户端代理代理类 ServiceDescriptionImporter sdi = new ServiceDescriptionImporter(); sdi.AddServiceDescription(sd, null, null); //使用 CodeDom 编译客户端代理类 CodeNamespace cn = new CodeNamespace(CODE_NAMESPACE); CodeCompileUnit ccu = new CodeCompileUnit(); ccu.Namespaces.Add(cn); sdi.Import(cn, ccu); Microsoft.CSharp.CSharpCodeProvider icc = new Microsoft.CSharp.CSharpCodeProvider(); CompilerParameters cp = new CompilerParameters(); CompilerResults cr = icc.CompileAssemblyFromDom(cp, ccu); agentType = cr.CompiledAssembly.GetTypes()[0]; agent = Activator.CreateInstance(agentType); } ///<summary< ///调用指定的方法 ///</summary< ///<param name="methodName"<方法名,大小写敏感</param< ///<param name="args"<参数,按照参数顺序赋值</param< ///<returns<Web服务的返回值</returns< public object Invoke(string methodName, params object[] args) { MethodInfo mi = agentType.GetMethod(methodName); return this.Invoke(mi, args); } ///<summary< ///调用指定方法 ///</summary< ///<param name="method"<方法信息</param< ///<param name="args"<参数,按照参数顺序赋值</param< ///<returns<Web服务的返回值</returns< public object Invoke(MethodInfo method, params object[] args) { return method.Invoke(agent, args); } public MethodInfo[] Methods { get { return agentType.GetMethods(); } } } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace WindowsApplication1 { public partial class Form1 : Form { private string _url = "http://www.baidu.com"; public Form1() { InitializeComponent(); init_Data(); } public void init_Data() { WindowsServiceWebDefaultHotCity.WebServiceAgent agent = new WindowsServiceWebDefaultHotCity.WebServiceAgent(_url); object[] args = new object[6]; args[0] = "PEK"; args[1] = "CAN"; args[2] = ""; args[3] = "2008-08-02"; args[4] = "00:00"; args[5] = "own_9588"; string text=agent.Invoke("GetAllFlight", args).ToString(); textBox1.Text = text; } } }
我们都知道,调用WS可以在工程中添加对WS的WEB引用。 但是,如果我们不想通过添加引用的方式,而是在代码中动态引用该怎么办呢? 首先,我们该想到WS的实现也是一个类的形式。 其次,WS在传输过程中是通过WSDL来进行描述的(使用SOAP协议)。 因此,我们需要获取WS的WSDL描述,并通过该描述来动态生成程序集。 最后:通过反射来获取新生成的程序集,并调用其方法! 上述步骤需要引用如下四个名称空间: using System.Web.Services.Description; //WS的描述 //以下几个用于根据描述动态生成代码并动态编译获取程序集 using System.CodeDom; using Microsoft.CSharp; using System.CodeDom.Compiler; 上述几个名称空间中包括如下几个重要的类: using System.Web.Services.Description下: ServiceDescription //WS描述 ServiceDescriptionImporter //通过描述生成客户端代理类,特别注意其中的Style 以下是MSDN对其的描述: XML Web services 的接口通常由 Web 服务描述语言 (WSDL) 文件来说明。例如,若要获取有关使用 http://localhost/service.asmx 处公开的 ASP.NET 的 Web 服务的 WSDL 说明,只需导航到 http://localhost/service.asmx?WSDL。使用 ServiceDescriptionImporter 类可以方便地将 WSDL 说明中包含的信息导入到 System.CodeDom.CodeCompileUnit 对象。通过调整 Style 参数的值,可以指示 ServiceDescriptionImporter 实例生成客户端代理类(通过透明调用该类可提供 Web 服务的功能)或生成抽象类(该类封装 Web 服务的功能而不实现该功能)。如果将 Style 属性设置为 Client,则 ServiceDescriptionImporter 生成客户端代理类,通过调用这些类来提供说明的 Web 服务的功能。如果将 Style 属性设置为 Server,则 ServiceDescriptionImporter 实例生成抽象类,这些类表示所说明的 XML Web services 的功能而不进行实现。然后,可以通过编写从这些抽象类继承的类来对其进行实现,并实现相关的方法。 using System.CodeDom下: CodedomUnit //它用于设定动态代码的名称空间,类名等,可以通过ServiceDescriptionImporter.Import()方法将WS的描述代码写入该类,以作动态编译用
using System.CodeDom.Compiler下:
CodedomProvider //用于创建和检索代码生成器和代码编译器的实例,我们主要用到其实现子类CShareCodeProvider
可以直接用CShareCodeProvider provider=new CShareCodeProvider()来生成,或者用CodedomProvider.CreateProvider("CSharp")来生成
ICodeCompiler //用于编译基于 System.CodeDom 的源代码表示形式。
它通过CodedomProvider的CreateCompiler()方法来
CompilerResults //表示从编译器返回的编译结果。 它由ICodeCompiler根据指定的编译器设置从指定的 CodeCompileUnit 所包含的 System.CodeDom 树中编译程序集并返回。CompiledAssembly 属性指示编译的程序集。
了解如上信息后,就可动态调用WS了。
如下是摘自http://www.cnblogs.com/ruochen/archive/2007/12/11/990427.html的代码演示:
Code
该方法可以使程序不通过web引用的方式去调用webservices方法,直接在代码里调用该方法就能达到动态调用webservices的目的。使用前先引用System.Web.Services动态链接库,是.net自带的dll。
方法如下:
using System; using System.Collections.Generic; using System.Text; using System.Net; using System.IO; using System.Web.Services.Description; using System.CodeDom; using Microsoft.CSharp; using System.CodeDom.Compiler; namespace TestSkin { class Webservices { /// <summary< /// 实例化WebServices /// </summary< /// <param name="url"<WebServices地址</param< /// <param name="methodname"<调用的方法</param< /// <param name="args"<把webservices里需要的参数按顺序放到这个object[]里</param< public static object InvokeWebService(string url, string methodname, object[] args) { //这里的namespace是需引用的webservices的命名空间,在这里是写死的,大家可以加一个参数从外面传进来。 string @namespace = "client"; try { //获取WSDL WebClient wc = new WebClient(); Stream stream = wc.OpenRead(url + "?WSDL"); ServiceDescription sd = ServiceDescription.Read(stream); string classname = sd.Services[0].Name; ServiceDescriptionImporter sdi = new ServiceDescriptionImporter(); sdi.AddServiceDescription(sd, "", ""); CodeNamespace cn = new CodeNamespace(@namespace); //生成客户端代理类代码 CodeCompileUnit ccu = new CodeCompileUnit(); ccu.Namespaces.Add(cn); sdi.Import(cn, ccu); CSharpCodeProvider csc = new CSharpCodeProvider(); ICodeCompiler icc = csc.CreateCompiler(); //设定编译参数 CompilerParameters cplist = new CompilerParameters(); cplist.GenerateExecutable = false; cplist.GenerateInMemory = true; cplist.ReferencedAssemblies.Add("System.dll"); cplist.ReferencedAssemblies.Add("System.XML.dll"); cplist.ReferencedAssemblies.Add("System.Web.Services.dll"); cplist.ReferencedAssemblies.Add("System.Data.dll"); //编译代理类 CompilerResults cr = icc.CompileAssemblyFromDom(cplist, ccu); if (true == cr.Errors.HasErrors) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach (System.CodeDom.Compiler.CompilerError ce in cr.Errors) { sb.Append(ce.ToString()); sb.Append(System.Environment.NewLine); } throw new Exception(sb.ToString()); } //生成代理实例,并调用方法 System.Reflection.Assembly assembly = cr.CompiledAssembly; Type t = assembly.GetType(@namespace + "." + classname, true, true); object obj = Activator.CreateInstance(t); System.Reflection.MethodInfo mi = t.GetMethod(methodname); return mi.Invoke(obj, args); } catch { return null; } } } }
===了解上述类和方法后,基本就可以动态调用WS了。
特别注意的是:动态编译后需要用到反射来读取并执行。因此需要您了解什么是反射及如何反射。
web service的动态调用,主要有三种方法。
1、修改config文件。只要你引用了web service,就会在config文件中出现asmx文件的地址。只需要修改该地址即可。
2、程序修改url。web service是集成了System.Web.Service.WebService类的,而该类有一个Url属性。通过修改该属性可以达到与方法一一样的效果,并且比它还要灵活。有的时候,我们需要提供一个列表,有很多的服务器让用户选择。程序根据用户的选择连接到不同的服务器上调用web service。这时,就可以用这个方法了。
3、接口引用。有的时候,我们需要调用不同服务器上的web service,但他们彼此又不一样,只是都实现了同一个接口。这时候,就可以考虑下面的这个方法。只要先引用所有的web service,然后用接口实例来保存创建出来的web service对象即可。
4、动态编译。这个应该算得上是真正意义上的动态了。有的时候,各个服务器上的web service更新比较快,我们不可能天天去更新代理类的,这个时候就可以用这个方法了。
在该方法中,有一点限制。就是各个服务器的web service,要么是都继承了同一个接口,要么是都有一些同样签名的方法,而且service的类名最好是一样的。不过就算不符合这条件也没关系,后面我会在注释中注明的。看代码:
using System; using System.CodeDom; using System.CodeDom.Compiler; using System.IO; using System.Net; using System.Reflection; using System.Web.Services.Description; using Microsoft.CSharp; //获取Web Service描述 WebClient wc= new WebClient(); Stream stream = wc.OpenRead("http://localhost/TestService.asmx?WSDL"); //这里指定你自己的web service url,一定要以?WSDL结尾 ServiceDescription sd = ServiceDescription.Read(stream); ServiceDescriptionImporter sdi = new ServiceDescriptionImporter(); sdi.ProtocolName = "soap"; sdi.Style = ServiceDescriptionImportStyle.Client; sdi.AddServiceDescription(sd, null, null); //指定命名空间 CodeNamespace cn = new CodeNamespace("Test"); //这里随便指定一个命名空间,但要与后面的一致 CodeCompileUnit ccu = new CodeCompileUnit(); ccu.Namespaces.Add(cn); sdi.Import(cn, ccu); 建立C#编译器 CSharpCodeProvider csc = new CSharpCodeProvider(); ICodeCompiler icc = csc.CreateCompiler(); CompilerParameters cp = new CompilerParameters(); cp.GenerateExecutable = false; cp.GenerateInMemory = true; //添加编译条件 cp.ReferencedAssemblies.Add("System.dll"); cp.ReferencedAssemblies.Add("System.XML.dll"); cp.ReferencedAssemblies.Add("System.Web.Services.dll"); //编译程序集 CompilerResults cr = icc.CompileAssemblyFromDom(cp, ccu); //检查是否编译成功 if (!cr.Errors.HasErrors) { //编译成功 //获取程序集 Assembly assembly = cr.CompiledAssembly; //获取程序集类型 //前面的Test就是命名空间,必须要与前面指定的一致 //后面的TestService就是service的类名 //如果所有的服务器都是一致的类名,这里就可以写死,否则要动态提供类名 Type type = assembly.GetType("Test.TestService", true); object service = Activator.CreateInstance(type); //获取方法 //如果所有的服务器都是一致的方法名,这里可以写死,否则就要动态提供方法名 MethodInfo mi = type.GetMethod("HelloWorld"); //调用方法 //如果方法没有参数,第二个参数可以传递null,否则就要传递object数组,数组元素的顺序要与参数的顺序一致 //如果所有服务器的方法签名都是一致的,object数组的顺序就可以写死了,否则还要动态调整元素的数量及顺序 mi.Invoke(service, null); //最后,返回的是object类型,根据方法的签名,把返回值转换成不同的对象即可。 } else { //这里自己处理编译错误 }