博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【MVC 4】7.SportsSore:完成购物车
阅读量:5961 次
发布时间:2019-06-19

本文共 19901 字,大约阅读时间需要 66 分钟。

本文将继续构建 SportsStore 示例应用程序。在上一章中,添加了对购物车的基本支持,现在打算改善并完成其功能。

1.使用模型绑定

 MVC 框架使用了一个叫作“模型绑定”的系统,以便通过 HTTP 请求来创建一些 C# 对象,目的是把它们作为参数值传递给动作方法。例如,MVC 处理表单的方式就是这样。框架会考察目标动作方法的参数,并用一个模型绑定器来获取表单中 input 元素的值,并把它们转换成同名的参数类型。

模型绑定器能够通过请求中可用的信息来创建 C# 类型,这是 MVC 框架的核心特性之一。本节将创建一个自定义模型绑定器来盖上 CartController 类。

人们喜欢使用 Cart 控制器中的会话状态特性来存储和管理 Cart 对象,但却不喜欢它要采取的工作方式。它不符合本应用程序模型的其余部分,而那是基于动作方法参数的(因为动作方法参数的操作以模型为基础,而会话状态的操作需要设置键值对,两者的工作方式不一致)。另外。除非模仿基类的 Session 参数,否则不能适当的对 CartController 类进行单元测试,而这意味着需要模仿 Controller 类(控制器的基类),以及其他一些不希望处理的东西。

为了解决这一问题,我们打算创建一个自定义模型绑定器,以获得包含在会话数据中的 Cart 对象(注意,常规的模型绑定器能够直接处理请求中的数据来创建模型对象,这里创建自定义绑定器的目的是为了处理会话中的数据,手工用会话数据创建 Cart 对象)。然后,MVC 框架能够创建 Cart 对象,并把它们作为参数传递给 Controller 类的动作方法。

创建自定义模型绑定器

通过实现 IModelBinder 接口,可以创建一个自定义模型绑定器。在 SportsStore.WebUI 项目中新建文件夹“Binders”,并新建类文件 CartModelBinder.cs ,代码如下:

 

using SportsStore.Domain.Entities;using System.Web.Mvc;namespace SportsStore.WebUI.Binders{    public class CartModelBinder : IModelBinder    {        private const string sessionKey = "Cart";        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)        {            //通过会话获取 Cart            Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey];            //若会话中没有 Cart ,则创建一个            if (cart == null)            {                cart = new Cart();                controllerContext.HttpContext.Session[sessionKey] = cart;            }            //返回 cart            return cart;        }    }}

这些 IModelBinder 接口定义了一个方法: BindModel 。所提供的两个参数使得创建域模型对象成为可能。 ControllerContext 对控制器类所具有的全部信息提供了访问,这些信息包含了客户端请求的细节。 ModelBindingContext 提供了要求建立的模型对象的信息,以及使绑定更易于处理的工具。

对于本例的目的而言,所关心的是 ControllerContext 类,它具有 HttpContext 属性,它又相应地有一个 Session 属性,该属性能够获取和设置会话数据。通过读取会话数据的键值可以获取 Cart,而在会话中还没有 Cart 时,又可以创建一个 Cart 。

需要告诉 MVC 框架,它可以使用 CartModelBinder 类来创建 Cart 的实例。这需要在 Global.asax 的 Application_Start 方法中进行注册:

using SportsStore.Domain.Entities;using SportsStore.WebUI.Binders;using SportsStore.WebUI.Infrastructure;using System.Web.Http;using System.Web.Mvc;using System.Web.Optimization;using System.Web.Routing;namespace SportsStore.WebUI{    public class MvcApplication : System.Web.HttpApplication    {        protected void Application_Start()        {            AreaRegistration.RegisterAllAreas();            WebApiConfig.Register(GlobalConfiguration.Configuration);            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);            RouteConfig.RegisterRoutes(RouteTable.Routes);            BundleConfig.RegisterBundles(BundleTable.Bundles);            ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());            ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());        }    }}

现在可以更新 CartController 类,删除 GetCart 方法而依靠现在的模型绑定器,MVC 框架会自动地应用它,代码如下:

using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using SportsStore.WebUI.Models;using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;namespace SportsStore.WebUI.Controllers{    public class CartController : Controller    {        private IProductRepository repository;        public CartController(IProductRepository repo)        {            repository = repo;        }        public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)        {            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);            if (product != null)            {                cart.AddItem(product, 1);            }            return RedirectToAction("Index", new { returnUrl });        }        public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)        {            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);            if (product != null)            {                cart.RemoveLine(product);            }            return RedirectToAction("Index", new { returnUrl });        }        public ViewResult Index(Cart cart, string returnUrl)        {            return View(new CartIndexViewModel            {
Cart = cart, ReturnUrl = returnUrl }); } }}

上面代码删除了 GetCart 方法,并对每个动作方法添加了 Cart 参数。当 MVC 框架接收到一个请求,比如,要求调用 AddToCart 方法时,会首先考察动作方法的参数,然后考察可用的绑定器列表,并试图找到一个能够创建每个参数类型实例的绑定器。这会要求自定义绑定器创建一个 Cart 对象,而这时通过利用会话状态特性来完成的。通过自定义绑定器的默认绑定器,MVC 框架能够创建一组调用动作方法所需要的参数,这让开发者能够重构控制器,以便在接收到请求时知道如何创建 Cart 对象。

像这样使用自定义模型绑定器有几个好处。第一个好处是把用来创建 Cart 与创建控制器的逻辑分离开来了,这让开发者能够修改存储 Cart 对象,而不需要修改控制器。第二个好处是任何使用 Cart 对象的控制器类,都能够简单地把这些对象声明为动作方法参数,并能够利用自定义模型绑定器。第三个好处是它能够对 Cart 控制器进行单元测试,而不需要模仿大量的ASP.NET 通道。

 

2.完成购物车

前面已经介绍了自定义模型绑定器,现在到了添加两个新特性来完成购物车功能的时候了。第一个特性将允许客户删除购物车物品,第二个特性将在页面的顶部显示购物车的摘要。

 

2.1 删除购物车物品

前面已经定义了控制器中的 RemoveFromCart 动作方法,因此,让客户删除物品只不过是在视图中将这个方法暴露出来的事情。本文打算在购物车摘要的每一行中添加一个“Remove”按钮来做这件事。对 Views/Cart/Index.cshtml 所做的修改如下:

        @foreach (var line in Model.Cart.Lines)        {                            @line.Quantity                @line.Product.Name                @line.Product.Price.ToString("c")                @((line.Quantity * line.Product.Price).ToString("c"))                                    @using (Html.BeginForm("RemoveFromCart", "Cart"))                    {                        @Html.Hidden("ProductId", line.Product.ProductID)                        @Html.HiddenFor(x => x.ReturnUrl)                                            }                                    }    

注:可以使用强类型的 Html.HiddenFor 辅助器方法,为 ReturnUrl 模型属性创建一个隐藏字段,但这需要使用基于字符串的 Html.Hidden 辅助器方法,对 ProductID 字段做同样的事情。 如果写成“Html.HiddenFor(x=>line.Product.ProductID)”,该辅助器方法便会渲染一个以“line.Product.ProductID”为名称的隐藏字段。该字段名与 CartController.RemoveFromCart 动作方法的参数名不匹配,这会使默认的模型绑定器无法工作,因此 MVC 框架便不能调用此方法了。

运行应用程序,可以看到效果:

 

2.2 添加购物车摘要

现在已经实现了一个功能化的购物车,但将该购物车集成到解密的方式还存在一个问题:只有通过查看购物车摘要屏幕,客户才能知道他们的购物车里有些什么。而且,他们只能通过把一个新的物品加入购物车,才能看到购物车的摘要屏幕。

为了解决这一问题,本节打算添加一个小部件,它汇总购物车的内容,并能够通过点击来显示购物车内容。下面将采用与添加导航不见十分相似的方式来完成这一工作——作为一个动作,把它的输出注入到 Razor 布局。

首先,需要对 CartController 类添加一个简单的方法,代码如下:

public PartialViewResult Summary(Cart cart)        {            return PartialView(cart);        }

可以看出,这是一个简单的方法。它只需要渲染一个视图,以当前 Cart (它是自定义模型绑定器获得的)作为视图数据。还需要一个分部视图,它在对这个 Summary 方法调用做出响应时被渲染。添加对应的 Summary 视图文件,代码如下:

@model SportsStore.Domain.Entities.Cart
Your cart: @Model.Lines.Sum(x => x.Quantity) item(s), @Model.ComputeTotalValue().ToString("c") @Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery }, null)

这是一个简单的视图,它显示了购物车的物品数、这些物品的总费用,以及把购物车内容显示给用户的一个链接。现在,已经定义了有 Summary 动作方法所返回的这个视图,可以在 _Layout.cshtml 文件中包含它的渲染结果,代码如下:

    
@ViewBag.Title
@{Html.RenderAction("Menu", "Nav");}
@RenderBody()

最后一步是添加一些 CSS规则,对该分部视图中的元素进行格式化。对 SportsStore.WebUI 项目中的 Site.css 文件添加样式文件如下:

div#cart {
float:right;margin:.8em;color:silver;background-color:#555;padding:.5em .5em .5em 1em;}div#cart a {
text-decoration:none;padding:.4em 1em .4em 1em;line-height:2.1em;margin-left:.5em;background-color:#333;color:white;border:1px solid black;}

运行程序即可以看到效果,对购物车添加物品时,物品数以及总费用都会增加,如下图所示:

利用这个附件,现在可以让客户知道自己的购物车中有什么。这个附件也显示的提供了一个结算离店的办法。从中再一次看到用 RenderAction 把一个动作方法所渲染的输出组合到一个 Web 页面是多么容易。这是将应用程序功能分解成清晰可重用模块的一种很好的技术。

 

3.递交订单

现在到了显示 SportsStore 最后一个客户特性的时候了:结算并完成订单的能力。下面将扩充域模型,以提供收集用户送货细节的支持,并添加一个处理这些细节的特性。

 

3.1 扩充域模型

在 SportsStore.Domain 项目的 Entities 文件夹中新建类 ShippingDetails 。这是用来表示客户送货细节的类,具体代码如下:

 

using System.ComponentModel.DataAnnotations;namespace SportsStore.Domain.Entities{    public class ShippingDetails    {        [Required(ErrorMessage = "Please enter a name")]        public string Name { get; set; }        [Required(ErrorMessage = "Please enter the first address line")]        public string Line1 { get; set; }        public string Line2 { get; set; }        public string Line3 { get; set; }        [Required(ErrorMessage = "Please enter a city name")]        public string City { get; set; }        [Required(ErrorMessage = "Please enter a state name")]        public string State { get; set; }        public string Zip { get; set; }        [Required(ErrorMessage = "Please enter a country name")]        public string Country { get; set; }        public bool GiftWrap { get; set; }    }}

上述代码利用了 System.ComponentModel.DataAnnotations 命名空间的验证注解属性,正如之前文章  所做的那样。

 

3.2 添加结算过程

本例的目的是到达一个程序节点,以此作为用户输入其送货细节并递交订单的入口。为此,需要在购物车摘要视图上添加一个“Checkout now”按钮。修改 Views/Cart/Index.cshtml 文件,修改代码如下:

...

Continue shopping @Html.ActionLink("Checkout now", "Checkout")

...

黄色为修改部分,这一修改生成了一个链接,点击这个链接时,调用 Cart 控制器的 Checkout 动作方法。显示效果如下:

 

现在需要在 CartController 类中定义这个 Checkout 方法:

public ViewResult Checkout(){   return View(new ShippingDetails());}

此 Checkout 方法返回默认视图,并传递一个新的 ShippingDetails 对象作为视图模型。为了创建相应的视图文件 Views/Cart/Checkout.cshtml ,视图内容如下:

@model SportsStore.Domain.Entities.ShippingDetails@{    ViewBag.Title = "SportsStore: Checkout";}

Check out now

Please enter your details, and we'll ship your goods right away!@using (Html.BeginForm()){

Ship to

Name:@Html.EditorFor(x => x.Name)

Address

Line 1: @Html.EditorFor(x => x.Line1)
Line 2: @Html.EditorFor(x => x.Line2)
Line 3: @Html.EditorFor(x => x.Line3)
City: @Html.EditorFor(x => x.City)
State: @Html.EditorFor(x => x.State)
Zip: @Html.EditorFor(x => x.Zip)
Country: @Html.EditorFor(x => x.Country)

Options

}

运行程序,可以看到该视图是如何渲染的,效果如下图所示,该视图为收集客户的送货细节渲染了一个表单。

本例用 Html.EditorFor 辅助器方法为每个表单字段渲染了一个 input 元素。该方法是模板化辅助器方法的一个例子(注意,对模型对象的每个属性使用的都是 Html.EditorFor 辅助器方法,并未针对各个属性的类型,去选定特定的辅助器方法)。它让 MVC 框架去决定一个视图模型属性需要采用哪种 input 元素,而不是进行明确的指定(例如,使用 Html.TextBoxFor)。

从显示效果可以看出, MVC 框架为布尔属性渲染了一个复选框,如“Gift wrap these items”选择,而对那些字符串属性渲染了文本框。

 

3.3 实现订单处理器

 在这个应用程序中还需要一个组件,以便能够对订单的细节进行处理。为了与 MVC 模型原理保持一致,本例打算为此功能定义一个借口、编写该接口的一个实现,然后用 DI 容器 Ninject 把两者关联起来。

 

定义接口

在 SportsStore.Domain 项目的 Abstract 文件夹中新建接口 IOrderProcessor ,内容如下:

using SportsStore.Domain.Entities;namespace SportsStore.Domain.Abstract{    public interface IOrderProcessor    {        void ProcessOrder(Cart cart, ShippingDetails shippingDetails);    }}

 

实现接口

IOrderProcessor 的实现打算采用的订单处理方式是向网站管理员发送订单右击。当然,这简化了销售过程。大多数电子商务网站不会简单的发送订单邮件,而且也没有提供信用卡处理或其他支付形式的支持,只是希望把事情维持在关注 MVC 方面,因此采用了这种发送邮件作为订单处理的方式。

在 SportsStore.Domain 项目的 Concrete 文件夹中新建类 EmailOrderProcessor,这个类使用了包含在 .NET 框架中内建的 SMTP(简单邮件传输协议)支持,以发送一份电子邮件。具体代码如下:

 

using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using System.Net;using System.Net.Mail;using System.Text;namespace SportsStore.Domain.Concrete{    public class EmailSettings    {        public string MailToAddress = "orders@example.com";        public string MailFromAddress = "sportsstore@example.com";        public bool UserSsl = true;        public string Username = "MySmtpUsername";        public string Password = "MySmtpPassword";        public string ServerName = "smtp.example.com";        public int ServerPort = 587;        public bool WriteAsFile = false;        public string FileLocation = @"c:\sports_store_emails";    }    public class EmailOrderProcessor : IOrderProcessor    {        private EmailSettings emailSettings;        public EmailOrderProcessor(EmailSettings settings)        {            emailSettings = settings;        }        public void ProcessOrder(Cart cart, ShippingDetails shippingInfo)        {            using (var smtpClient = new SmtpClient())            {                smtpClient.EnableSsl = emailSettings.UserSsl;                smtpClient.Host = emailSettings.ServerName;                smtpClient.Port = emailSettings.ServerPort;                smtpClient.UseDefaultCredentials = false;                smtpClient.Credentials = new NetworkCredential(emailSettings.Username, emailSettings.Password);                if (emailSettings.WriteAsFile)                {                    smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;                    smtpClient.PickupDirectoryLocation = emailSettings.FileLocation;                    smtpClient.EnableSsl = false;                }                StringBuilder body = new StringBuilder()                    .AppendLine("A new order has been submitted")                    .AppendLine("---")                    .AppendLine("Items:");                foreach (var line in cart.Lines)                {                    var subTotal = line.Product.Price * line.Quantity;                    body.AppendFormat("{0} x {1} (subtotal: {2:c})", line.Quantity, line.Product.Name, subTotal);                }                body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue())                    .AppendLine("---")                    .AppendLine("Ship to:")                    .AppendLine(shippingInfo.Name)                    .AppendLine(shippingInfo.Line1)                    .AppendLine(shippingInfo.Line2 ?? "")                    .AppendLine(shippingInfo.Line3 ?? "")                    .AppendLine(shippingInfo.City)                    .AppendLine(shippingInfo.State ?? "")                    .AppendLine(shippingInfo.Country)                    .AppendLine(shippingInfo.Zip)                    .AppendLine("---")                    .AppendFormat("Gift wrap: {0}", shippingInfo.GiftWrap ? "Yes" : "No");                MailMessage mailmessage = new MailMessage(                    emailSettings.MailFromAddress,//Form                    emailSettings.MailToAddress,//To                    "New order submitted!",//Subject                    body.ToString());//Body                if (emailSettings.WriteAsFile)                {                    mailmessage.BodyEncoding = Encoding.ASCII;                }                smtpClient.Send(mailmessage);            }        }    }}

为了使事情更简单些,也定义了 EmailSettings 类。 EmailOrderProcessor 的构造器需要这个类(EmailSettings 类)的一个实例,该实例包含了配置 .NET 邮件类所需要的全部设置信息。

提示:如果没有可用的 SMTP 服务器也没关系,可以将 EmailSettings.WriteAsFile属性设置为true,这样会把邮件消息作为文件写到由 FileLocation 属性指定的目录。该目录必须依据存在且是可写入的。邮件文件的扩展名将为 .eml ,但它们可以被任何文本编辑器所读取。

 

3.4 注册(接口)实现

现在,有了 IOrderProcessor 接口的一个实现以及配置它的手段,便可以用 Ninject 来创建它的实例。编辑 SportsStore.WebUI 项目中的 NinjectController 类(在 Infrastructure 文件夹中),对 AddBindings 方法进行修改:

using Moq;using Ninject;using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using System;using System.Linq;using System.Collections.Generic;using System.Web.Mvc;using System.Web.Routing;using SportsStore.Domain.Concrete;using System.Configuration;namespace SportsStore.WebUI.Infrastructure{    public class NinjectControllerFactory : DefaultControllerFactory    {        private IKernel ninjectKernel;        public NinjectControllerFactory()        {            ninjectKernel = new StandardKernel();            AddBindings();        }        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)        {            return controllerType == null                ? null                : (IController)ninjectKernel.Get(controllerType);        }        private void AddBindings()        {            //put bindings here            ninjectKernel.Bind
().To
(); EmailSettings emailSettings = new EmailSettings { WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false") }; ninjectKernel.Bind
().To
().WithConstructorArgument("settings", emailSettings); } }}

上述代码创建了一个 EmailSettings 对象,将其用于 Ninject 的 WithConstructorArgument 方法,以便在需要创建一个新实例对 IOrderProcessor 接口的请求进行服务时,把它注入到 EmailOrderProcessor 构造器中。上述代码只为 EmailSettings 中一个属性 WriteAsFiles 指定了值。使用 ConfigurationManager.AppSettings 属性来读取该属性的值,这让用户能够访问已经放在 Web.config 文件中的应用程序,在应用程序配置文件 Web.config 中读取这个 WriteAsFile 属性的设置值,这也是从应用程序配置文件 Web.config 中读取某个属性设置值的方法。配置文件修改如下:

 

3.5 完成购物车控制器

为了完成 CartController 类,需要修改构造器,以使它要求 IOrderProcessor 接口的一个实现,并添加一个新的动作方法,它将在客户点击“Complete order”按钮时,处理 HTTP 表单的 POST 请求。

using SportsStore.Domain.Abstract;using SportsStore.Domain.Entities;using SportsStore.WebUI.Models;using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;namespace SportsStore.WebUI.Controllers{    public class CartController : Controller    {        private IProductRepository repository;        private IOrderProcessor orderProcessor;        public CartController(IProductRepository repo, IOrderProcessor proc)        {            repository = repo;            orderProcessor = proc;        }        public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)        {            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);            if (product != null)            {                cart.AddItem(product, 1);            }            return RedirectToAction("Index", new { returnUrl });        }        public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)        {            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);            if (product != null)            {                cart.RemoveLine(product);            }            return RedirectToAction("Index", new { returnUrl });        }        public ViewResult Index(Cart cart, string returnUrl)        {            return View(new CartIndexViewModel            {                Cart = cart,                ReturnUrl = returnUrl            });        }        public PartialViewResult Summary(Cart cart)        {            return PartialView(cart);        }        [HttpPost]        public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)        {            if (cart.Lines.Count() == 0)            {                ModelState.AddModelError("", "Sorry,your cart is empty!");            }            if (ModelState.IsValid)            {                orderProcessor.ProcessOrder(cart, shippingDetails);                cart.Clear();                return View("Completed");            }            else            {                return View(shippingDetails);            }        }        public ViewResult Checkout()        {            return View(new ShippingDetails());        }    }}

 

可以看出,这里所添加的 Checkout 动作方法是用 HttpPost 注解属性来修饰的,这表明该方法将用于对 POST 请求的处理 —— 在此例中,这是用户递交表单的时候。再次重申,ShippingDetails 参数(这是使用 HTTP 表单数据自动创建的)和 Cart 参数(这是用自定义绑定器创建的)都要依赖于模型绑定器系统。

注:构造器中的修改迫使用户需要对 CartController 类创建的单元测试进行更新。为新的构造器参数传递 null ,便会使单元测试能够通过编译。

在上述代码中, MVC 框架会检查验证约束,这些约束是用数据注解属性而运用于 ShippingDetails 的,并通过 ModelState 属性把非法情况传递给该动作方法。因此,可以通过检查 Model.IsValid 属性来查看是否存在问题。注意,如果购物车中无物品,还调用 ModelState.AddModelError 方法注册了一条错误消息。

 

3.6 显示验证错误

如果客户输入了非法的错误信息,有问题的那些非法表单字段将被高亮,但没有消息被显示出来。更糟的是,如果客户试图对一个空购物车进行结算,这不会完成这个订单,但客户却根本看不到任何错误信息。为了解决这个问题,需要对视图添加一个验证摘要。下面代码显示了添加到 Checkout.cshtml 视图的内容。

... 

Check out now

Please enter your details, and we'll ship your goods right away!@using (Html.BeginForm()){ @Html.ValidationSummary();

Ship to

Name:@Html.EditorFor(x => x.Name)
...

现在,当客户提供非法送货数据或试图对空购物车进行结算时,系统会向他们显示一些有用的错误消息,如下图所示:

 

3.7 显示致谢界面

为了完成结算过程,需要向客户显示一个已经完成订单处理的确认页面,并感谢他们的购物。新建视图文件 Views/Cart/Completed.cshtml ,代码如下:

@{    ViewBag.Title = "SportsStore: OrderSubmitted";}

Thanks!

Thanks for placing your order. We'll ship your goods as soon as possible.

现在,客户可以进行从选择产品到结算离开的整个购物过程。如果客户提供有效的的送货细节(且购物车中有物品),当他们点击“Complete order”按钮时,便会看到一个致谢页面,如下图所示:

本文转自叶超Luka博客园博客,原文链接:http://www.cnblogs.com/yc-755909659/p/5344007.html,如需转载请自行联系原作者
你可能感兴趣的文章
maven自动发布到服务器
查看>>
比对2个文件夹,删除已存在,或者 删除指定目录下,按文字开头(结束)文件
查看>>
uva 712 - S-Trees
查看>>
git 保存账号密码
查看>>
angularjs + fis +modJS 对于支持amd规范的组建处理(PhotoSwipe 支持,百度webUpload支持)...
查看>>
Spring Retry
查看>>
struts2源码分析-IOC容器的实现机制(上篇)
查看>>
meta之viewport
查看>>
手写SpringIoc底层实现代码,基于反射机制
查看>>
JPA学习笔记1——JPA基础
查看>>
Mac下Qt Creator无法输入中文的解决方法
查看>>
分享Android编程中Facebook图片加载库Fresco的使用
查看>>
【转载】知乎技术方案初探
查看>>
【转载】MySQL复制的概述、安装、故障、技巧、工具
查看>>
JXL导出Excel数据表
查看>>
数据库字段加1
查看>>
GNOME是最好的!
查看>>
Nginx 的软件负载均衡详解
查看>>
如何解决mysql主从延迟
查看>>
iOS App 之间的相互跳转
查看>>