热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【UnityApwork框架】AOP编程拦截,用于缓存和异常处理(Unity框架的拦截注入Interception)...

第一步:定义拦截行为:CachingBehavior和ExceptionLoggingBehavior他们都继承接口:IIntercept

第一步:定义拦截行为:CachingBehavior 和 ExceptionLoggingBehavior

           他们都继承接口:IInterceptionBehavior (程序集 Microsoft.Practices.Unity.Interception.dll, v2.1.505.0

                                                                  命名空间:Microsoft.Practices.Unity.InterceptionExtension)

           需要实现连个接口:

public IEnumerable GetRequiredInterfaces()public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)

 

CachingBehavior.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Keasy5.Infrastructure.Caching;
using Microsoft.Practices.Unity.InterceptionExtension;namespace Keasy5.Infrastructure.InterceptionBehaviors
{
///

/// 表示用于方法缓存功能的拦截行为。/// public class CachingBehavior : IInterceptionBehavior{#region Private Methods/// /// 根据指定的以及实例,/// 获取与某一特定参数值相关的键名。/// /// 实例。/// 实例。/// 与某一特定参数值相关的键名。/// /// 例如:/// /// private string GetValueKey(CachingAttribute cachingAttribute, IMethodInvocation input){switch (cachingAttribute.Method){// 如果是Remove,则不存在特定值键名,所有的以该方法名称相关的缓存都需要清除case CachingMethod.Remove:return null;// 如果是Get或者Put,则需要产生一个针对特定参数值的键名case CachingMethod.Get:case CachingMethod.Put:if (input.Arguments != null &&input.Arguments.Count > 0){var sb = new StringBuilder();for (int i = 0; i ){sb.Append(input.Arguments[i].ToString());if (i != input.Arguments.Count - 1)sb.Append("_");}return sb.ToString();}elsereturn "NULL";default:throw new InvalidOperationException("无效的缓存方式。");}}#endregion#region IInterceptionBehavior Members/// /// 获取当前行为需要拦截的对象类型接口。/// /// 所有需要拦截的对象类型接口。public IEnumerable GetRequiredInterfaces(){return Type.EmptyTypes;}/// /// 通过实现此方法来拦截调用并执行所需的拦截行为。/// /// 调用拦截目标时的输入信息。/// 通过行为链来获取下一个拦截行为的委托。/// 从拦截目标获得的返回信息。public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext){var method = input.MethodBase;var key = method.Name;if (method.IsDefined(typeof(CachingAttribute), false)){var cachingAttribute = (CachingAttribute)method.GetCustomAttributes(typeof(CachingAttribute), false)[0];var valKey = GetValueKey(cachingAttribute, input);switch (cachingAttribute.Method){case CachingMethod.Get:try{if (CacheManager.Instance.Exists(key, valKey)){var obj = CacheManager.Instance.Get(key, valKey);var arguments = new object[input.Arguments.Count];input.Arguments.CopyTo(arguments, 0);return new VirtualMethodReturn(input, obj, arguments);}else{var methodReturn = getNext().Invoke(input, getNext);CacheManager.Instance.Add(key, valKey, methodReturn.ReturnValue);return methodReturn;}}catch (Exception ex){return new VirtualMethodReturn(input, ex);}case CachingMethod.Put:try{var methodReturn = getNext().Invoke(input, getNext);if (CacheManager.Instance.Exists(key)){if (cachingAttribute.Force){CacheManager.Instance.Remove(key);CacheManager.Instance.Add(key, valKey, methodReturn.ReturnValue);}elseCacheManager.Instance.Put(key, valKey, methodReturn.ReturnValue);}elseCacheManager.Instance.Add(key, valKey, methodReturn.ReturnValue);return methodReturn;}catch (Exception ex){return new VirtualMethodReturn(input, ex);}case CachingMethod.Remove:try{var removeKeys = cachingAttribute.CorrespondingMethodNames;foreach (var removeKey in removeKeys){if (CacheManager.Instance.Exists(removeKey))CacheManager.Instance.Remove(removeKey);}var methodReturn = getNext().Invoke(input, getNext);return methodReturn;}catch (Exception ex){return new VirtualMethodReturn(input, ex);}default: break;}}return getNext().Invoke(input, getNext);}/// /// 获取一个值,该值表示当前拦截行为被调用时,是否真的需要执行/// 某些操作。/// public bool WillExecute{get { return true; }}#endregion}
}
View Code

 

ExceptionLoggingBehavior.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Practices.Unity.InterceptionExtension;namespace Keasy5.Infrastructure.InterceptionBehaviors
{
///

/// 表示用于异常日志记录的拦截行为。/// public class ExceptionLoggingBehavior : IInterceptionBehavior{#region IInterceptionBehavior Members/// /// 获取当前行为需要拦截的对象类型接口。/// /// 所有需要拦截的对象类型接口。public IEnumerable GetRequiredInterfaces(){return Type.EmptyTypes;}/// /// 通过实现此方法来拦截调用并执行所需的拦截行为。/// /// 调用拦截目标时的输入信息。/// 通过行为链来获取下一个拦截行为的委托。/// 从拦截目标获得的返回信息。public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext){var methodReturn = getNext().Invoke(input, getNext);if (methodReturn.Exception != null){Utils.Log(methodReturn.Exception);}return methodReturn;}/// /// 获取一个值,该值表示当前拦截行为被调用时,是否真的需要执行/// 某些操作。/// public bool WillExecute{get { return true; }}#endregion}
}
View Code

 

第二步:添加配置文件,为需要被拦截的类添加拦截行为。

例如如下的配置,是为实现了接口

Keasy5.ServiceContract.IProductService

的类的所以方法添加拦截行为。

web/app.config文件:

"http://schemas.microsoft.com/practices/2010/unity">"Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />"Interception" />
。。。。。
"Keasy5.ServiceContract.IProductService, Keasy5.ServiceContract" mapTo="Keasy5.Application.Implementation.ProductServiceImpl, Keasy5.Application">"InterfaceInterceptor" />"Keasy5.Infrastructure.InterceptionBehaviors.CachingBehavior, Keasy5.Infrastructure" />"Keasy5.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, Keasy5.Infrastructure" />。。。。

 

比如:对于ExceptionLoggingBehavior的拦截行为作用于IProductService接口的实现类的所有方法:

///

/// 通过实现此方法来拦截调用并执行所需的拦截行为。/// /// 调用拦截目标时的输入信息。/// 通过行为链来获取下一个拦截行为的委托。/// 从拦截目标获得的返回信息。public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext){var methodReturn = getNext().Invoke(input, getNext);if (methodReturn.Exception != null){Utils.Log(methodReturn.Exception);}return methodReturn;}

效果是:如果IProductService接口的实现类的所有方法如果抛出了异常,将调用   

Utils.Log(methodReturn.Exception);

将异常信息写入日志系统(这就是我们进行拦截的目的)。

------------------------------------------------

对应缓存CachingBehavior的拦截的补充:

第一:定义特性:CachingAttribute

CachingAttribute.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;namespace Keasy5.Infrastructure.Caching
{
///

/// 表示由此特性所描述的方法,能够获得来自基础结构层所提供的缓存功能。/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]public class CachingAttribute : Attribute{#region Ctor/// /// 初始化一个新的CachingAttribute类型。/// /// 缓存方式。public CachingAttribute(CachingMethod method){this.Method = method;}/// /// 初始化一个新的CachingAttribute类型。/// /// 缓存方式。/// /// 与当前缓存方式相关的方法名称。注:此参数仅在缓存方式为Remove时起作用。/// public CachingAttribute(CachingMethod method, params string[] correspondingMethodNames): this(method){this.CorrespondingMethodNames = correspondingMethodNames;}#endregion#region Public Properties/// /// 获取或设置缓存方式。/// public CachingMethod Method { get; set; }/// /// 获取或设置一个值,该值表示当缓存方式为Put时,是否强制将值写入缓存中。/// public bool Force { get; set; }/// /// 获取或设置与当前缓存方式相关的方法名称。注:此参数仅在缓存方式为Remove时起作用。/// public string[] CorrespondingMethodNames { get; set; }#endregion}
}
View Code

 

第二:将特性CachingAttribute应用于IProductService的接口方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;
using ByteartRetail.DataObjects;
using Keasy5.DataObject;
using Keasy5.Infrastructure;
using Keasy5.Infrastructure.Caching;namespace Keasy5.ServiceContract
{
///

/// 表示与“商品”相关的应用层服务契约。/// [ServiceContract(Namespace = "http://www.ByteartRetail.com")]public interface IProductService : IApplicationServiceContract{#region Methods/// /// 创建商品信息。/// /// 需要创建的商品信息。/// 已创建的商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetProductsForCategory","GetProductsWithPagination","GetFeaturedProducts","GetProductsForCategoryWithPagination","GetProducts","GetProductByID")]ProductDataObjectList CreateProducts(ProductDataObjectList productDataObjects);/// /// 创建商品分类。/// /// 需要创建的商品分类。/// 已创建的商品分类。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetCategories")]CategoryDataObjectList CreateCategories(CategoryDataObjectList categoryDataObjects);/// /// 更新商品信息。/// /// 需要更新的商品信息。/// 已更新的商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetProductsForCategory","GetProductsWithPagination","GetFeaturedProducts","GetProductsForCategoryWithPagination","GetProducts", "GetProductByID")]ProductDataObjectList UpdateProducts(ProductDataObjectList productDataObjects);/// /// 更新商品分类。/// /// 需要更新的商品分类。/// 已更新的商品分类。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetCategories", "GetCategoryByID")]CategoryDataObjectList UpdateCategories(CategoryDataObjectList categoryDataObjects);/// /// 删除商品信息。/// /// 需要删除的商品信息的ID值。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetProductsForCategory","GetProductsWithPagination","GetFeaturedProducts","GetProductsForCategoryWithPagination","GetProducts", "GetProductByID")]void DeleteProducts(IDList productIDs);/// /// 删除商品分类。/// /// 需要删除的商品分类的ID值。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetCategories", "GetCategoryByID")]void DeleteCategories(IDList categoryIDs);/// /// 设置商品分类。/// /// 需要进行分类的商品ID值。/// 商品分类ID值。/// 带有商品分类信息的对象。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetProductsForCategory","GetProductsForCategoryWithPagination")]CategorizationDataObject CategorizeProduct(Guid productID, Guid categoryID);/// /// 取消商品分类。/// /// 需要取消分类的商品ID值。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Remove, "GetProductsForCategory","GetProductsForCategoryWithPagination")]void UncategorizeProduct(Guid productID);/// /// 根据指定的ID值获取商品分类。/// /// 商品分类ID值。/// 查询方式。/// 商品分类。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]CategoryDataObject GetCategoryByID(Guid id, QuerySpec spec);/// /// 获取所有的商品分类。/// /// 查询方式。/// 所有的商品分类。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]CategoryDataObjectList GetCategories(QuerySpec spec);/// /// 根据指定的ID值获取商品信息。/// /// 商品信息ID值。/// 查询方式。/// 商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]ProductDataObject GetProductByID(Guid id, QuerySpec spec);/// /// 获取所有的商品信息。/// /// 查询方式。/// 商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]ProductDataObjectList GetProducts(QuerySpec spec);/// /// 以分页的方式获取所有商品信息。/// /// 带有分页参数信息的对象。/// 经过分页的商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]ProductDataObjectListWithPagination GetProductsWithPagination(Pagination pagination);/// /// 根据指定的商品分类ID值,获取该分类下所有的商品信息。/// /// 商品分类ID值。/// 所有的商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]ProductDataObjectList GetProductsForCategory(Guid categoryID);/// /// 根据指定的商品分类ID值,以分页的方式获取该分类下所有的商品信息。/// /// 商品分类ID值。/// 带有分页参数信息的对象。/// 所有的商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]ProductDataObjectListWithPagination GetProductsForCategoryWithPagination(Guid categoryID, Pagination pagination);/// /// 获取所有的特色商品信息。/// /// 需要获取的特色商品信息的个数。/// 特色商品信息。
[OperationContract][FaultContract(typeof(FaultData))][Caching(CachingMethod.Get)]ProductDataObjectList GetFeaturedProducts(int count);#endregion}
}
View Code

 

第三:拦截IProductService接口的实现类的所有方法的所有方法,

并通过CachingAttribute特性进行方法 的筛选,

还进一步根据CachingAttribute的属性值进行一步的处理:

CachingBehavior .cs

///

/// 通过实现此方法来拦截调用并执行所需的拦截行为。/// /// 调用拦截目标时的输入信息。/// 通过行为链来获取下一个拦截行为的委托。/// 从拦截目标获得的返回信息。public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext){var method = input.MethodBase;var key = method.Name;if (method.IsDefined(typeof(CachingAttribute), false)){var cachingAttribute = (CachingAttribute)method.GetCustomAttributes(typeof(CachingAttribute), false)[0];var valKey = GetValueKey(cachingAttribute, input);switch (cachingAttribute.Method){case CachingMethod.Get:try{if (CacheManager.Instance.Exists(key, valKey)){var obj = CacheManager.Instance.Get(key, valKey);var arguments = new object[input.Arguments.Count];input.Arguments.CopyTo(arguments, 0);return new VirtualMethodReturn(input, obj, arguments);}else{var methodReturn = getNext().Invoke(input, getNext);CacheManager.Instance.Add(key, valKey, methodReturn.ReturnValue);return methodReturn;}}catch (Exception ex){return new VirtualMethodReturn(input, ex);}case CachingMethod.Put:try{var methodReturn = getNext().Invoke(input, getNext);if (CacheManager.Instance.Exists(key)){if (cachingAttribute.Force){CacheManager.Instance.Remove(key);CacheManager.Instance.Add(key, valKey, methodReturn.ReturnValue);}elseCacheManager.Instance.Put(key, valKey, methodReturn.ReturnValue);}elseCacheManager.Instance.Add(key, valKey, methodReturn.ReturnValue);return methodReturn;}catch (Exception ex){return new VirtualMethodReturn(input, ex);}case CachingMethod.Remove:try{var removeKeys = cachingAttribute.CorrespondingMethodNames;foreach (var removeKey in removeKeys){if (CacheManager.Instance.Exists(removeKey))CacheManager.Instance.Remove(removeKey);}var methodReturn = getNext().Invoke(input, getNext);return methodReturn;}catch (Exception ex){return new VirtualMethodReturn(input, ex);}default: break;}}return getNext().Invoke(input, getNext);}

 

--------------------------------

unity服务定位器:ServiceLocator

ServiceLocator.cs 

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;namespace Keasy5.Infrastructure
{
///

/// Represents the Service Locator./// public sealed class ServiceLocator : IServiceProvider{#region Private Fieldsprivate readonly IUnityContainer container;#endregion#region Private Static Fieldsprivate static readonly ServiceLocator instance = new ServiceLocator();#endregion#region Ctor/// /// Initializes a new instance of ServiceLocator class./// private ServiceLocator(){UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");container = new UnityContainer();section.Configure(container);}#endregion#region Public Static Properties/// /// Gets the singleton instance of the ServiceLocator class./// public static ServiceLocator Instance{get { return instance; }}#endregion#region Private Methodsprivate IEnumerable GetParameterOverrides(object overridedArguments){List overrides = new List();Type argumentsType = overridedArguments.GetType();argumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList().ForEach(property =>{var propertyValue = property.GetValue(overridedArguments, null);var propertyName = property.Name;overrides.Add(new ParameterOverride(propertyName, propertyValue));});return overrides;}#endregion#region Public Methods/// /// Gets the service instance with the given type./// /// The type of the service./// The service instance.public T GetService(){return container.Resolve();}public IEnumerable ResolveAll(){return container.ResolveAll();}/// /// Gets the service instance with the given type by using the overrided arguments./// /// The type of the service./// The overrided arguments./// The service instance.public T GetService(object overridedArguments){var overrides = GetParameterOverrides(overridedArguments);return container.Resolve(overrides.ToArray());}/// /// Gets the service instance with the given type by using the overrided arguments./// /// The type of the service./// The overrided arguments./// The service instance.public object GetService(Type serviceType, object overridedArguments){var overrides = GetParameterOverrides(overridedArguments);return container.Resolve(serviceType, overrides.ToArray());}#endregion#region IServiceProvider Members/// /// Gets the service instance with the given type./// /// The type of the service./// The service instance.public object GetService(Type serviceType){return container.Resolve(serviceType);}#endregion}
}
View Code

 

 

 

 

转:https://www.cnblogs.com/easy5weikai/p/3790931.html



推荐阅读
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • Todayatworksomeonetriedtoconvincemethat:今天在工作中有人试图说服我:{$obj->getTableInfo()}isfine ... [详细]
  • Asp.net Mvc Framework 七 (Filter及其执行顺序) 的应用示例
    本文介绍了在Asp.net Mvc中应用Filter功能进行登录判断、用户权限控制、输出缓存、防盗链、防蜘蛛、本地化设置等操作的示例,并解释了Filter的执行顺序。通过示例代码,详细说明了如何使用Filter来实现这些功能。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
author-avatar
做普通的自我
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有