WCF技术剖析之二十一:WCF基本异常处理模式[中篇]
原文:WCF技术剖析之二十一:WCF基本异常处理模式[中篇]
通过WCF基本的异常处理模式[上篇], 我们知道了:在默认的情况下,服务端在执行某个服务操作时抛出的异常(在这里指非FaultException异常),其相关的错误信息仅仅限于服务端可见,并不会被WCF传递到客户端;如果将开启了IncludeExceptionDetailInFaults的ServiceDebug服务行为通过声明(通过在服务类型上应用ServiceBehaviorAttrite特性)或者配置的方式应用到相应的服务上,异常相关的所有细节信息将会原封不动地向客户端传送。
这两种方式体现了两种极端的异常传播(Exception Propagation)机制,对于基于服务操作执行过程中抛出的异常的错误细节,要么完全对客户端屏蔽,要么全部暴露于客户端。在真正通过WCF来架构我们的分布式系统中,我们往往需要一种折中的异常传播机制:自定义服务端异常信息。这样既可以让客户端得到一个易于理解的错误信息,又在一定程度上避免了一些敏感信息的泄露。
一、 通过FaultException直接指定错误信息
对于执行服务操作中抛出的异常,如果服务的定义者仅仅希望服务的调用者得到一段自定义的错误信息文本(字符串),我们要做的实际上很简单:在服务操作中直接抛出一个FaultException异常,该异常对象通过以字符串形式体现的自定义错误信息创建。下面的代码中,CalculaorService的Divide方式在指定的时候对第二参数进行了验证,如果为零则创建一个FaultException,并指定错误信息(“被除数y不能为零!”)。
1: using System.ServiceModel;
2: using Artech.WcfServices.Contracts;
3: namespace Artech.WcfServices.Services
4: {
5: [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
6: public class CalculatorService : ICalculator
7: {
8: public int Divide(int x, int y)
9: {
10: if (0 == y)
11: {
12: throw new FaultException("被除数y不能为零!");
13: }
14: return x / y;
15: }
16: }
17: }
客户端在调用该服务操作的时候,如果传入零作为被除数,将会直接捕获服务端定义的抛出的这个异常(实际上,这其中经历了异常对象的序列化、消息交换以及异常对象的反序列化等一系列的操作)。客户端具体的异常捕获情况如下面的程序体现:
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
11: "calculatorservice"))
12: {
13: ICalculator calculator = channelFactory.CreateChannel();
14: using (calculator as IDisposable)
15: {
16: try
17: {
18: int result = calculator.Divide(1, 0);
19: }
20: catch (FaultException ex)
21: {
22: Console.WriteLine(ex.Message);
23: (calculator as ICommunicationObject).Abort();
24: }
25: }
26: }
27: }
28: }
29: }
输出结果:
被除数y不能为零!
虽然在很多情况下,在服务端指定服务操作的过程中直接抛出含有自定义错误信息的FaultException异常,就能过客户端感知到遇到的具体错误并进行必要的排错和纠错。但是,我们更多地,还是倾向于直接定义一个类型来描述异常信息。我个人倾向于这样一类的类型为错误明细类型(Fault Detail Type)。服务端根据具体的异常场景创建相应的错误类型对象,并基于该对象我们上面提到的System.ServiceModel.FaultException<TDetail>异常,其中泛型类型参数为异常细节类型。在这个过程中,还涉及到一个重要的概念:错误契约(Fault Contract),接下来,我们就来介绍一下FaultException<TDetail>和错误契约。
二、 通过FaultException<TDetail>采用自定义类型封装错误
由于用于封装错误信息的异常细节类型的对象最终需要通过消息交换的方式从服务端传播到客户端,所以该对象必须是一个可序列化的对象。WCF通过两种典型序列化器实现对数据对象的序列化和反序列化,其中一个是传统的System.Xml.Serialization.XmlSerializer,该序列换器被ASP.NET Web服务用于对象和XML之间的序列化和反序列化;另一个则是System.Runtime.Serialization.DataContractSerializer,用于基于数据契约对象的序列化和反序列化,后者是WCF默认采用的序列化器。所以,可序列化的错误明细类型一般是一个数据契约,或者是一个应用了System.SerializableAttribute特性的类型。关于序列化,和与此相关的数据契约、数据契约序列化器等,在《WCF技术剖析(卷1)》的第5章有深入、全面的介绍。
我们仍然用我们上面提到的计算服务来举例,现在我们需要定义一个独立的类型来描述基于CalculatorService的异常,我们索性将该类型起名为CalculationError。我们将CalculationError定义成一个应用了System.Runtime.Serialization.DataContractAttribute特性的数据契约,简单起见,我们仅仅定义了两个数据成员(应用了System.Runtime.Serialization.DataMemberAttribute特性):Operation表示导致异常相应的运算操作(我们假设CalculatorService具有一系列运算操作,虽然我们的例子中仅仅给出为一一个除法运算操作:Divide),而Message表述具体的错误消息。CalculationError的定义在被客户端(Client项目)和服务(Services项目)引用的契约(Contracts项目)中,具体定义如下:
1: using System;
2: using System.Runtime.Serialization;
3: namespace Artech.WcfServices.Contracts
4: {
5: [DataContractAttribute(Namespace="http://www.artech.com/")]
6: public class CalculationError
7: {
8: public CalculationError(string operation,string message)
9: {
10: if (string.IsNullOrEmpty(operation))
11: {
12: throw new ArgumentNullException("operation");
13: }
14: if (string.IsNullOrEmpty(message))
15: {
16: throw new ArgumentNullException("message");
17: }
18: this.Operation = operation;
19: this.Message = message;
20: }
21: [DataMember]
22: public string Operation
23: { get; set; }
24: [DataMember]
25: public string Message
26: { get; set; }
27: }
28: }
照理说,我们已经正确定义了错误明细类型CalculationError,在CalculatorService的Divide操作中就可以直接抛出一个Fault<CalculationError>,并将一个创建一个CalculationError对象作为该异常对象的明细(通过Detail属性表示),具体的代码如下所示:
1: using System.ServiceModel;
2: using Artech.WcfServices.Contracts;
3: namespace Artech.WcfServices.Services
4: {
5: public class CalculatorService : ICalculator
6: {
7: public int Divide(int x, int y)
8: {
9: if (0 == y)
10: {
11: var error = new CalculationError("Divide", "被除数y不能为零!");
12: throw new FaultException<CalculationError>(error,error.Message);
13: }
14: return x / y;
15: }
16: }
17: }
客户端服务调用相关的异常处理也作如下相应的修改:
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
11: "calculatorservice"))
12: {
13: ICalculator calculator = channelFactory.CreateChannel();
14: using (calculator as IDisposable)
15: {
16: try
17: {
18: int result = calculator.Divide(1, 0);
19: }
20: catch (FaultException<CalculationError> ex)
21: {
22: Console.WriteLine("运算错误");
23: Console.WriteLine("运算操作:{0}",ex.Detail.Operation);
24: Console.WriteLine("错误消息:{0}",ex.Detail.Message);
25: (calculator as ICommunicationObject).Abort();
26: }
27: }
28: }
29: }
30: }
31: }
但是我们的客户端程序并不能按照我们实现预想的那样正常运行,而会抛出如图1所示的未被处理的FaultException异常,而我们试图捕获的异常类型为FaultException<CalculationError>。
图1 客户端不能正常捕获FaultException<CalculationError>异常
三、错误契约(Fault Contract)
要回答上面出错的原因,就需要谈到WCF或者SOA一个根本的特征:服务的提供者和消费者之间采用基于“契约(Contract)”的交互方式。不同于面向服务,在面向组件设计中,组件之间的交互实际上是基于类型的,交互双方需要共享相同类型集(接口、抽象类或者具体类等)。在《WCF技术剖析(卷1)》中,我们曾多次对契约进行过深入的探讨。从抽象层面上讲,契约时交互双方或者多方就某一问题达成的一种共识,使确保正常交互指定的一系列的规范。
从本质上讲,服务契约(Service Contract)中的每一个操作契约(Operation Contract),定义了WCF为实现该服务操作的调用采用的消息交换模式(MEP:Message Exchange Pattern),并且结合基于参数、返回值类型的数据契约、消息契约定义了请求消息和回复消息的结构(Schema)。数据契约建立了对相同数据的两种不同表现形式(托管对象和XML)之间的双向适配,以利于承载相同信息的数据在两种不同形态之间的转换,即序列换和反序列化。而消息契约在定义了托管对象的各个成员与相应的消息元素之间的匹配关系。借助于消息契约,在对一个托管对象进行序列化并生成消息的时候,可以有效地控制某一个数据成员(属性或者字段)被序列化成的XML应该置于消息报头(Header)还是消息主体(Body)。
总的来说,上述的这些契约基本上都是围绕着一个正常服务调用下的消息交换:服务的消费者通过向服务的提供者发送请求消息,服务的提供者在接受到该请求后,激活服务实例并调用相应的服务操作,最终将返回的结果以回复消息的方式返回给服务的消费者(对于One-way,则不需要消息的回复)。但是,如果服务操作不能正确地执行,服务端将会通过一种特殊的消息将错误信息返回给客户端,这种消息被称为错误消息(Fault Message)。对于错误消息,同样需要相应的契约来定义其结构,我们把这种契约称为错误契约(Fault Contract)。
WCF通过System.ServiceModel.FaultContractAttribute特性定义,由于错误契约是基于服务操作级别的,所以该特性直接应用于服务契约接口或者类的操作契约方法成员上。下面的代码片断体现了FaultContractAttribute的定义:
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
2: public sealed class FaultContractAttribute : Attribute
3: {
4: public FaultContractAttribute(Type detailType);
5: public string Action { get; set; }
6: public Type DetailType { get; }
7: public bool HasProtectionLevel { get; }
8: public string Name { get; set; }
9: public string Namespace { get; set; }
10: public ProtectionLevel ProtectionLevel { get; set; }
11: }
FaultContractAttribute的6个属性分别具有如下的含义:
- Action:和一般的SOAP消息一样,对于Fault SOAP,WS-Address报头Action是必须的,该属性控制Action报头的值。如果Action属性没有在应用FaultContractAttribute时显式指定,那么它将按照下面的规则进行指定:{服务契约命名空间}/{服务契约名称}/{操作契约名称}{明细类型名称}Fault;
- DetailType:也就是上面所介绍的用于封装错误信息的错误明细类型,比如我们前面定义的CalculationError;
- Name和Namespace:在最终的Fault SOAP中,错误明细对象被序列化成的XML将会被置于Fault SOAP的主体部分,而这两个属性则用于控制这段XML片断对应的名称和命名空间;如果这两个属性并未作显式设置,WCF将会使用DetailType对应的数据契约名称和命名空间;
- HasProtectionLevel和ProtectionLevel:这两个属性涉及到保护级别,属于安全(Security)的问题,在这里就不多作介绍了。
下面的代码中,我们通过FaultContractAttribute将我们定义错误明细类型CalculationError应用到Divide操作之上,这样的话,我们的例子就能够正常运行了。
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Contracts
3: {
4: [ServiceContract(Namespace = "http://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract]
8: [FaultContract(typeof(CalculationError))]
9: int Divide(int x, int y);
10: }
11: }
按照我们在上面提到的关于Action、Name和Namespace的默认设定,上面这段代码和下面的代码是完全等效的。
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Contracts
3: {
4: [ServiceContract(Name="ICalculator",Namespace = "http://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract(Name="Divide")]
8: [FaultContract(typeof(CalculationError),
9: Action = "http://www.artech.com/ICalculator/DivideCalculationErrorFault",
10: Name = "CalculationError", Namespace = "http://www.artech.com/")]
11: int Divide(int x, int y);
12: }
13: }
对于我们前面的例子,当客户端调用CalculatorService的Divide操作执行除法预算,并传入零作为被除数,服务端将会抛出FaultException<CalculationError>异常。WCF服务端框架将会产生一个Fault Message,并将序列化后的CalculationError对象作为错误明细放置到Fault Message的主体部分。如果采用的消息版本是Soap12Addressing10,即SOAP 1.2和WS-Addressing 1.0,最终生成的Fault Message将会如下面的XML片断所示:
1: <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
2: <s:Header>
3: <a:Action s:mustUnderstand="1">http://www.artech.com/ICalculator/DivideCalculationErrorFault</a:Action> <a:RelatesTo>urn:uuid:3498ba2d-edd0-4d3b-ba4a-9b35327b5fa3</a:RelatesTo>
4: </s:Header>
5: <s:Body>
6: <s:Fault>
7: <s:Code>
8: <s:Value>s:Sender</s:Value>
9: </s:Code>
10: <s:Reason>
11: <s:Text xml:lang="zh-CN">被除数y不能为零!</s:Text>
12: </s:Reason>
13: <s:Detail>
14: <CalculationError xmlns="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
15: <Message>被除数y不能为零!</Message>
16: <Operation>Divide</Operation>
17: </CalculationError>
18: </s:Detail>
19: </s:Fault>
20: </s:Body>
21: </s:Envelope>
错误契约作为服务描述的一部分,会参与到描述服务的元数据(Metadata)中。当服务元数据通过WSDL的形式被发布的时候,作为对操作的描述的错误契约体现在WSDL的<wsdl:portType>/<wsdl:operation>/<wsdl:fault>节点。下面一段XML代表CalculatorService的WDSL:
1: <?xml version="1.0" encoding="utf-8"?>
2: <wsdl:definitions name="CalculatorService" targetNamespace="http://www.artech.com/" >
3: <wsdl:import namespace="http://tempuri.org/" location="http://127.0.0.1:3721/calculatorservice/mex?wsdl=wsdl0"/>
4: <wsdl:types>
5: <xs:schema elementFormDefault="qualified" targetNamespace="http://www.artech.com/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.artech.com/">
6: <xs:element name="Divide">
7: <xs:complexType>
8: <xs:sequence>
9: <xs:element minOccurs="0" name="x" type="xs:int"/>
10: <xs:element minOccurs="0" name="y" type="xs:int"/>
11: </xs:sequence>
12: </xs:complexType>
13: </xs:element>
14: <xs:element name="DivideResponse">
15: <xs:complexType>
16: <xs:sequence>
17: <xs:element minOccurs="0" name="DivideResult" type="xs:int"/>
18: </xs:sequence>
19: </xs:complexType>
20: </xs:element>
21: <xs:complexType name="CalculationError">
22: <xs:sequence>
23: <xs:element minOccurs="0" name="Message" nillable="true" type="xs:string"/>
24: <xs:element minOccurs="0" name="Operation" nillable="true" type="xs:string"/>
25: </xs:sequence>
26: </xs:complexType>
27: <xs:element name="CalculationError" nillable="true" type="tns:CalculationError"/>
28: </xs:schema>
29: </wsdl:types>
30: <wsdl:message name="ICalculator_Divide_InputMessage">
31: <wsdl:part name="parameters" element="tns:Divide"/>
32: </wsdl:message>
33: <wsdl:message name="ICalculator_Divide_OutputMessage">
34: <wsdl:part name="parameters" element="tns:DivideResponse"/>
35: </wsdl:message>
36: <wsdl:message name="ICalculator_Divide_CalculationErrorFault_FaultMessage">
37: <wsdl:part name="detail" element="tns:CalculationError"/>
38: </wsdl:message>
39: <wsdl:portType name="ICalculator">
40: <wsdl:operation name="Divide">
41: <wsdl:input wsaw:Action="http://www.artech.com/ICalculator/Divide" message="tns:ICalculator_Divide_InputMessage"/>
42: <wsdl:output wsaw:Action="http://www.artech.com/ICalculator/DivideResponse" message="tns:ICalculator_Divide_OutputMessage"/>
43: <wsdl:fault wsaw:Action="http://www.artech.com/ICalculator/DivideCalculationErrorFault" name="CalculationErrorFault" message="tns:ICalculator_Divide_CalculationErrorFault_FaultMessage"/>
44: </wsdl:operation>
45: </wsdl:portType>
46: <wsdl:service name="CalculatorService">
47: <wsdl:port name="CustomBinding_ICalculator" binding="i0:CustomBinding_ICalculator">
48: <soap12:address location="http://127.0.0.1:3721/calculatorservice"/>
49: <wsa10:EndpointReference> <wsa10:Address>http://127.0.0.1:3721/calculatorservice</wsa10:Address>
50: </wsa10:EndpointReference>
51: </wsdl:port>
52: </wsdl:service>
53: </wsdl:definitions>
对于错误契约的应用,还有一点需要特别说明:不仅仅是将自定义的错误明细类型(比如CalculationError)应用到相应的操作契约上,你需要显失地利用FaultContractAttribute特性将其应用到服务契约接口或者类中相应的操作方法上面,对于一些基元类型,比如Int32,String等,你也需要这样。也即是说,同样对于我们的计算服务的例子,如果服务端试图通过抛出一个FaultException<string>来提供错误(如下面的代码所示),客户端最后捕获到的仅仅是一个FaultException异常,而非FaultException<string>异常。
1: using System.ServiceModel;
2: using Artech.WcfServices.Contracts;
3: namespace Artech.WcfServices.Services
4: {
5: public class CalculatorService : ICalculator
6: {
7: public int Divide(int x, int y)
8: {
9: if (0 == y)
10: {
11: throw new FaultException<string>("被除数y不能为零!", "被除数y不能为零!");
12: }
13: return x / y;
14: }
15: }
16: }
在这种情况下,你需要做的仍然是在相应的操作上面,通过应用FaultContractAttribute特性指定String类型作为其DetailType,如下面的代码所示:
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Contracts
3: {
4: [ServiceContract(Namespace = "http://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract]
8: [FaultContract(typeof(string))]
9: int Divide(int x, int y);
10: }
11: }
从FaultContractAttribute的定义我们可以看出,该特性可以在同一个目标对象上面多次应用(AllowMultiple = true)。这也很好理解:对于同一个服务操作,可能具有不同的异常场景,在不同的情况下,需要抛出不同的异常。
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
2: public sealed class FaultContractAttribute : Attribute
3: {
4: //省略成员
5: }
但是,如果你在同一个操作方法上面应用了多了FaultContractAttribute特性的时候,需要遵循一系列的规则,我们将在《WCF基本异常处理模式(下篇)》中进行逐条介绍。
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
WCF技术剖析之二十一:WCF基本异常处理模式[中篇]的更多相关文章
- WCF技术剖析之二十一: WCF基本的异常处理模式[上篇]
原文:WCF技术剖析之二十一: WCF基本的异常处理模式[上篇] 由于WCF采用.NET托管语言(C#和NET)作为其主要的编程语言,注定以了基于WCF的编程方式不可能很复杂.同时,WCF设计的一个目 ...
- WCF技术剖析之二十一:WCF基本异常处理模式[下篇]
原文:WCF技术剖析之二十一:WCF基本异常处理模式[下篇] 从FaultContractAttribute的定义我们可以看出,该特性可以在同一个目标对象上面多次应用(AllowMultiple = ...
- WCF技术剖析之二十九:换种不同的方式调用WCF服务[提供源代码下载]
原文:WCF技术剖析之二十九:换种不同的方式调用WCF服务[提供源代码下载] 我们有两种典型的WCF调用方式:通过SvcUtil.exe(或者添加Web引用)导入发布的服务元数据生成服务代理相关的代码 ...
- WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模拟程序)
原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模拟程序) 基于HTTP-GET的元数据发布方式与基于WS-MEX原理类似,但是ServiceMetad ...
- WCF技术剖析之二十八:自己动手获取元数据[附源代码下载]
原文:WCF技术剖析之二十八:自己动手获取元数据[附源代码下载] 元数据的发布方式决定了元数据的获取行为,WCF服务元数据架构体系通过ServiceMetadataBehavior实现了基于WS-ME ...
- WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于WS-MEX的实现](提供模拟程序)
原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于WS-MEX的实现](提供模拟程序) 通过<如何将一个服务发布成WSDL[编程篇]>的介绍我们知道了如何可以通过编程或者配 ...
- WCF技术剖析之二十七: 如何将一个服务发布成WSDL[编程篇]
原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[编程篇] 对于WCF服务端元数据架构体系来说,通过MetadataExporter将服务的终结点导出成MetadataSet(参考< ...
- WCF技术剖析之二十六:如何导出WCF服务的元数据(Metadata)[扩展篇]
原文:WCF技术剖析之二十六:如何导出WCF服务的元数据(Metadata)[扩展篇] 通过<实现篇>对WSDL元素和终结点三要素的之间的匹配关系的介绍,我们知道了WSDL的Binding ...
- WCF技术剖析之二十六:如何导出WCF服务的元数据(Metadata)[实现篇]
原文:WCF技术剖析之二十六:如何导出WCF服务的元数据(Metadata)[实现篇] 元数据的导出就是实现从ServiceEndpoint对象向MetadataSet对象转换的过程,在WCF元数据框 ...
随机推荐
- 如何去掉Protel99se汉化菜单
说明: 安装Protel99se汉化包后,卸载后重新安装也无法去掉汉化菜单.本文介绍解决办法. 问题: 网上一些Protel99se的安装包内,在介绍Protel安装时,采用3大步的方法.第一步安装P ...
- java学习之xml
xml的处理有两种方式dom和Sax 其中dom有3套api ,分别是dom和jdom和dom4j package com.gh.xml; import java.io.File; import ja ...
- LibSVM笔记系列(3)——初学移植libsvm的C/C++版本
在LibSVM笔记系列(1)中已经提到在g++环境中编译LibSVM只需要一个make命令那样简单. 本文将介绍 (1)LibSVM的编译文件结构 (2)svm.h中重要数据结构及函数分析 (3)sv ...
- VS2005+WINDDK+Driver Studio 3.2个人总结
通过在网上搜索大量的资料,终于把环境搭建起来.对于我这样的驱动新手来说,理应把高手们的东西整理并总结下,方便以后的初学者. 这三个软件的安装顺序没有具体规定,也有高手推荐的顺序,我自己也是重复安装卸载 ...
- 【前端】CSS雪碧
百度百科:http://baike.baidu.com/link?url=jblMCCF77bq7egbJ-9SudRmvXdwlQVVOq5D9MEEniQgJR-Lqanfrnjzwmgu7ato ...
- iOS 拍照保存到相册
之前看了一些开源的代码,里面有一个功能,就是将图片下载到相册,仔细看了其中的代码,只有很简单的一句话,并且保存过后,还可以判断是否保存成功. 如下代码所示, 点击按钮,将self.imageView上 ...
- C#学习之-----再论委托
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...
- docker学习笔记13:Dockerfile 指令 WORKDIR介绍
Dockerfile中的WORKDIR指令用于指定容器的一个目录, 容器启动时执行的命令会在该目录下执行. 相当于设置容器的工作目录了.我们来看一个dockerfile文件 #test FROM ub ...
- Gradle构建Java Web应用:Servlet依赖与Tomcat插件(转)
Gradle的官方tutorial介绍了构建Java Web应用的基本方法.不过在使用Servlet做上传的时候会碰到问题.这里分享下如何通过Servlet上传文件,以及如何使用Gradle来构建相应 ...
- [置顶] 图书推荐:SQL Server 2012 T-SQL基础 Itzik Ben-Gan
经过近三个月的不懈努力,终于翻译完毕了.图书虽然是基础知识,但是,即使你已经使用T-SQL几年,很多地方还是能够弥补你的知识空白.大师级的人物写基础知识,或许你想知道这基础中还有哪些深奥,敬请期待吧. ...