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

springAOP(一)实现原理

spring内部使用了jdk动态代理、cglib(通过生成字节码的方式,继承目标类扩展目标类功能)两种方式实现了AOP框架。本篇先详细介绍

spring内部使用了jdk动态代理、cglib(通过生成字节码的方式,继承目标类扩展目标类功能)两种方式实现了AOP框架。本篇先详细介绍spring内部的AOP概念实体、之后介绍spring AOP的使用方式和原理

实现原理

spring内部使用了jdk动态代理、cglib这两种机制构建了整个AOP框架的基础

JDK动态代理

我们可以通过反射技术,为需要代理的目标对象,创造一个代理类出来,并且在代理类中执行我们所需要的逻辑,如:统计方法执行时间、打印日志。
相对于cglib技术,JDK动态代理存在两个比较明显的缺点:

  1. 目标对象必须是通过实现接口的类,才能创建代理
  2. 在运行时,使用反射进行扩展目标对象功能,性能会略低于cglib字节码技术的实现方式

// 一个需要进行代理的接口
public interface Greeting {void sayHi(String name);void sayByte(String name);
}// 接口实现类,即目标对象。
// 我们需要在不改变该实现类代码的基础上,在执行接口方法时,进行一些额外的功能
public class GreetingImpl implements Greeting {@Overridepublic void sayHi(String name) {System.out.println(name);}@Overridepublic void sayByte(String name) {System.out.println(name);}
}// 实现一个InvocationHandler,用于执行额外功能,并且调用目标对象的方法
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;/*** created by luhuancheng on 2018/11/17* @author luhuancheng*/
public class LogInvocationHandler implements InvocationHandler {private Object target;public LogInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("Before execute"); // 在执行目标对象方法之前,打印日志Object result = method.invoke(target, args); // 执行目标对象的方法System.out.println("After execute"); // 在执行目标对象方法之后,打印日志return result;}
}// 创建动态代理
public class DynamicProxy {public static void main(String[] args) {Greeting greeting = new GreetingImpl();Object proxyInstance = Proxy.newProxyInstance(DynamicProxy.class.getClassLoader(), new Class[]{Greeting.class}, new LogInvocationHandler(greeting));Greeting proxy = (Greeting) proxyInstance;proxy.sayHi("luhuancheng");proxy.sayByte("luhuancheng");}
}

cglib字节码技术

当我们要代理的目标对象,并不是由一个接口实现时。我们无法通过JDK动态代理来进行代理对象的创建,这时候就需要cglib这种字节码技术的登场了。

// 需要被代理的目标类,该类没有实现任何一个接口
public class Requestable {public void request() {System.out.println("request in Requestable without implementint any interface");}
}import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;// 实现cglib的方法拦截器接口
public class RequestableCallback implements MethodInterceptor {private static final String INTERCEPTOR_METHOD_NAME = "request";@Overridepublic Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {if (INTERCEPTOR_METHOD_NAME.equals(method.getName())) {System.out.println("Before execute");Object result = methodProxy.invokeSuper(target, args);System.out.println("After execute");return result;}return null;}
}import net.sf.cglib.proxy.Enhancer;// cglib代理实现-动态字节码生成技术扩展对象行为(对目标对象进行继承扩展)
public class CglibProxy {public static void main(String[] args) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(Requestable.class);enhancer.setCallback(new RequestableCallback());Requestable proxy = (Requestable) enhancer.create();proxy.request();}
}

AOP概念实体

JoinPoint

JoinPoint,可以理解为执行切面动作的一个时机。如:构造方法调用时,字段设置时,方法调用时,方法执行时。但在spring AOP中,仅支持方法执行时的JoinPoint。

类图

Pointcut

Pointcut,是我们在开发AOP功能时,定义的一个帮助我们捕捉系统中的相应JoinPoint的规则。

类图

  1. ClassFilter, 用于匹配被执行织入操作的对象。如果类型对于我们要捕获的JoinPoint无关的话,可以使用TruePointcut类
  2. MethodMatcher,用于匹配被执行织入操作的方法 。从大的分类可以分为两类:StaticMethodMatcher(不关心Pointcut具体参数)、DynamicMethodMatcher(关系Pointcut具体参数)
  3. StaticMethodMatcherPointcut继承了StaticMethodMatcher而且实现接口Pointcut,其自身可以作为Pointcut和MethodMatcher
  4. NameMatchMethodPointcut,根据指定的方法名和JoinPoint方法名进行匹配
  5. JdkRegexpMethodPointcut,使用正则表达式与JoinPoint方法名进行匹配
  6. AnnotationMatchingPointcut,根据目标类中是否存在指定类型的注解来匹配JoinPoint
  7. ComposablePointcut,可以进行逻辑运算的Pointcut
  8. DynamicMethodMatcherPointcut,继承了DynamicMethodMatcher而且实现接口Pointcut,其自身可以作为Pointcut和MethodMatcher

Advice

Advice,定义了AOP功能中的横切逻辑

类图

BeforeAdvice

横切逻辑将在相应的JoinPoint执行之前执行

AfterAdvice

横切逻辑将在相应的JoinPoint执行之后执行。该接口又派生了两个子接口ThrowsAdvice、AfterReturningAdvice

ThrowsAdvice

横切逻辑将在相应的JoinPoint执行异常时执行

// ThrowsAdvice是一个没有定义方法的标记接口,但是在横切逻辑执行时,
// spring AOP内部会通过反射的方式,检测ThrowsAdvice实现类的方法。
// 我们可以定义以下三个方法,分别处理不同的异常
public class ExceptionBarrierThrowsAdvice implements ThrowsAdvice {public void afterThrowing(Throwable t) {// 普通异常处理逻辑}public void afterThrowing(RuntimeException e) {// 运行时异常处理逻辑}public void afterThrowing(Method method, Object[] args, Object target, ApplicationException e) {// 处理应用程序生成的异常}}/*** 业务异常类*/
class ApplicationException extends RuntimeException {}

AfterReturningAdvice

横切逻辑将在相应的JoinPoint正常执行返回时执行

MethodInterceptor

作为Spring AOP的环绕方法(Around Advice),可以拦截相应的JoinPoint方法执行,从而在JoinPoint执行的前、正常返回、执行后这三个地方进行横切逻辑的切入

public class PerformanceMethodInterceptor implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {long start = System.currentTimeMillis();try {// 调用目标方法return invocation.proceed();} finally {System.out.println(String.format("cost time: %d", System.currentTimeMillis() - start));}}
}

IntroductionInterceptor

Spring AOP框架中实现Introduction的接口。Introduction功能可以在不改变类的定义的情况下,为目标类增加新的接口或属性

Advisor(对应AOP世界中的切面,Aspect。)

Advisor,代表了spring中的Aspect。
分为两大类:PointcutAdvisor、IntroductionAdvisor

类图

PointcutAdvisor

PointcutAdvisor,默认存在三个实现类DefaultPointcutAdvisor、NameMatchMethodPointcutAdvisor、RegexpMethodPointcutAdvisor

DefaultPointcutAdvisor

DefaultPointcutAdvisor作为PointcutAdvisor比较通用的一个实现类,我们可以为其设置Pointcut、Advice。伪代码如下:

Pointcut pointcut = ...
Advice advice = ...
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);

NameMatchMethodPointcutAdvisor

NameMatchMethodPointcutAdvisor,其内部持有一个NameMatchMethodPointcut实例作为pointcut。可以通过setMappedName(String mappedName)或者setMappedNames(String... mappedNames)操作pointcut,设置匹配的Pointcut方法名

RegexpMethodPointcutAdvisor

RegexpMethodPointcutAdvisor,其内部持有一个AbstractRegexpMethodPointcut实现类的实例pointcut(默认为JdkRegexpMethodPointcut),使用setPattern(String pattern)或者setPatterns(String... patterns)设置正则表达式用于匹配JoinPoint方法名

DefaultBeanFactoryPointcutAdvisor

DefaultBeanFactoryPointcutAdvisor,间接继承了BeanFactoryAware,其内部持有beanfactory。在指定advice时,可以通过方法setAdviceBeanName(String adviceBeanName)指定advice在beanfactory中的唯一name。之后在需要Advice时,将从beanfactory中获取,减少了容器启动初期Advice和Advisor之间的耦合

IntroductionAdvisor

DefaultIntroductionAdvisor

DefaultIntroductionAdvisor作为唯一的Introduction类型的Advice,只能使用于Introduction场景。

spring AOP织入原理

在spring AOP中,可以通过ProxyFactory、ProxyFactoryBean、AbstractAutoProxyCreator(及其实现类)来执行织入

ProxyFactory

我们来看看ProxyFactory这个最基本的织入器是如何工作的。其步骤大致分为以下几步:

  1. 将目标对象传入构造器,实例化一个ProxyFactory
  2. 调用ProxyFactory相关方法,设置所需的参数。如:是否使用cglib优化setOptimize(true)、代理类setProxyTargetClass(true)
  3. 指定要织入的接口(这个步骤可选,代码内部会根据目标对象检测到接口)
  4. 实例化切面(即Advisor),设置切面逻辑(将Advice实现类实例设置进Advisor实现类中)
  5. 调用ProxyFactory.getProxy()获取代理类
  6. 执行代理类
基于接口

/**
* 基于接口的织入 - JDK动态代理
*/

private static void forInterface() {// 1. 需要被拦截的接口实现类ITask task = new TaskImpl();// 2. 实例化一个执行织入操作的ProxyFactoryProxyFactory weaver = new ProxyFactory(task);// 我们也可以让基于接口的织入,使用cglib的方式weaver.setProxyTargetClass(true);weaver.setOptimize(true);// 3. 指定需要织入的接口weaver.setInterfaces(ITask.class);// 4. 定义切面NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();advisor.setMappedName("execute");// 5. 指定切面逻辑advisor.setAdvice(new PerformanceMethodInterceptor());// 6. 将切面添加到ProxyFactory实例中weaver.addAdvisor(advisor);// 7. 执行织入,返回织入后的代理实例ITask proxyObject = (ITask) weaver.getProxy();// 8. 调用接口,此时的执行逻辑中织入了切面逻辑proxyObject.execute();System.out.println(proxyObject);System.out.println(task);
}

基于类

/**
* 基于类的织入 - CGLIB
*/

private static void forClass() {// 1. 实例化一个执行织入操作的ProxyFactory,作为织入器ProxyFactory weaver = new ProxyFactory(new Excutable());// 2. 实例化Advisor(切面)NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();advisor.setMappedName("execute");// 3. 为Advisor设置切面逻辑advisor.setAdvice(new PerformanceMethodInterceptor());// 4. 为织入器设置切面weaver.addAdvisor(advisor);// 5. 执行织入Excutable proxyObject = (Excutable) weaver.getProxy();// 6. 调用接口,此时的执行逻辑中织入了切面逻辑proxyObject.execute(); // 使用cglib,第一次执行需要动态生成字节码,效率比动态代理低。proxyObject.execute(); // 再次使用cglib,直接执行第一次调用生成的字节码,效率比动态代理高System.out.println(proxyObject.getClass());
}

Introduction织入

private static void forIntroduction() {ProxyFactory weaver = new ProxyFactory(new DevloperImpl());weaver.setInterfaces(IDevloper.class, ITester.class);TesterFeatureIntroductionInterceptor advice = new TesterFeatureIntroductionInterceptor();weaver.addAdvice(advice);
// DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(advice);
// weaver.addAdvisor(advisor);Object proxy = weaver.getProxy();((ITester) proxy).test();((IDevloper) proxy).develSoftware();
}

ProxyFactory内部机制

类图
AopProxy

AopProxy对使用不同实现机制的代理进行了抽象。提供两个接口用于生成代理对象

Object getProxy();
Object getProxy(ClassLoader classLoader);

ProxyConfig

提供了5个属性用于配置控制代理对象生成的过程

private boolean proxyTargetClass = false;private boolean optimize = false;boolean opaque = false;boolean exposeProxy = false;private boolean frozen = false;

Advised

提供接口用于配置生成代理的必要配置信息,比如Advice、Advisor等

AopProxyFactory

作为AopProxy的抽象工厂

DefaultAopProxyFactory

AopProxyFactory默认实现,通过接口根据AdvisedSupport提供的配置信息创建代理

AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;

ProxyFactory

作为基本的织入器,继承了ProxyCreatorSupport(为了统一管理生成不同类型的AopProxy,将生成逻辑抽象到了这个类中)。ProxyCreatorSupport持有了一个AopProxyFactory类型的实例,默认为DefaultAopProxyFactory

主流程

ProxyFactoryBean(容器内织入)

使用ProxyFactory我们可以独立于spring的IOC容器来使用spring AOP框架。但是便于我们管理Pointcut、Advice等相关的bean,我们一般利用IOC容器来进行管理。在IOC容器中,使用ProxyFactoryBean来进行织入

类图
可以看出ProxyFactoryBean实现了接口FactoryBean,其实现如下:

public class ProxyFactoryBean extends ProxyCreatorSupportimplements FactoryBean<Object>, BeanClassLoaderAware, BeanFactoryAware {private synchronized void initializeAdvisorChain() throws AopConfigException, BeansException {if (this.advisorChainInitialized) {return;}// 当拦截器名称列表不为空时&#xff0c;初始化advisor chainif (!ObjectUtils.isEmpty(this.interceptorNames)) {// 需要根据interceptorName从beanfactory中取到对应advisor的实例&#xff0c;所以beanfactory不能为nullif (this.beanFactory &#61;&#61; null) {throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " &#43;"- cannot resolve interceptor names " &#43; Arrays.asList(this.interceptorNames));}// Globals can&#39;t be last unless we specified a targetSource using the property...if (this.interceptorNames[this.interceptorNames.length - 1].endsWith(GLOBAL_SUFFIX) &&this.targetName &#61;&#61; null && this.targetSource &#61;&#61; EMPTY_TARGET_SOURCE) {throw new AopConfigException("Target required after globals");}// Materialize interceptor chain from bean names.for (String name : this.interceptorNames) {if (logger.isTraceEnabled()) {logger.trace("Configuring advisor or advice &#39;" &#43; name &#43; "&#39;");}// 如果name以符号"*"结尾&#xff0c;则从beanfactory中获取beanname为name&#xff08;去掉*&#xff09;开头的所有Advisor、Interceptor类型的bean&#xff0c;注册为Advisorif (name.endsWith(GLOBAL_SUFFIX)) {if (!(this.beanFactory instanceof ListableBeanFactory)) {throw new AopConfigException("Can only use global advisors or interceptors with a ListableBeanFactory");}addGlobalAdvisor((ListableBeanFactory) this.beanFactory,name.substring(0, name.length() - GLOBAL_SUFFIX.length()));}// 普通的name&#xff08;即非*号匹配的bean&#xff09;&#xff0c;则直接从beanfactory获取&#xff0c;添加到advisor chain中else {// If we get here, we need to add a named interceptor.// We must check if it&#39;s a singleton or prototype.Object advice;if (this.singleton || this.beanFactory.isSingleton(name)) {// Add the real Advisor/Advice to the chain.advice &#61; this.beanFactory.getBean(name);}else {// It&#39;s a prototype Advice or Advisor: replace with a prototype.// Avoid unnecessary creation of prototype bean just for advisor chain initialization.advice &#61; new PrototypePlaceholderAdvisor(name);}addAdvisorOnChainCreation(advice, name);}}}// 标记已初始化this.advisorChainInitialized &#61; true;}private synchronized Object getSingletonInstance() {if (this.singletonInstance &#61;&#61; null) {// 根据targetName从beanfactory中获取目标对象this.targetSource &#61; freshTargetSource();if (this.autodetectInterfaces && getProxiedInterfaces().length &#61;&#61; 0 && !isProxyTargetClass()) {// Rely on AOP infrastructure to tell us what interfaces to proxy.Class targetClass &#61; getTargetClass();if (targetClass &#61;&#61; null) {throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");}// 自动识别目标对象的接口&#xff0c;设置到interfaces属性中setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));}// Initialize the shared singleton instance.super.setFrozen(this.freezeProxy);// 生成代理对象this.singletonInstance &#61; getProxy(createAopProxy());}return this.singletonInstance;}&#64;Overridepublic Object getObject() throws BeansException {// 初始化advisor chaininitializeAdvisorChain();if (isSingleton()) {// 获取单例的代理return getSingletonInstance();}else {if (this.targetName &#61;&#61; null) {logger.warn("Using non-singleton proxies with singleton targets is often undesirable. " &#43;"Enable prototype proxies by setting the &#39;targetName&#39; property.");}// 获取原型的代理&#xff08;每次都会重新生成代理对象&#xff09;return newPrototypeInstance();}}
}

使用AbstractAutoProxyCreator实现类&#xff08;自动织入&#xff09;

从类图可以看出&#xff0c;所有的AutoProxyCreator都间接实现了接口InstantiationAwareBeanPostProcessor。而InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation方法提供了一个机会&#xff0c;让注册到beanfactory中的bean在实例化之前&#xff0c;可以有一个创建实例的嵌入逻辑。具体可以看下springIOC容器实例化bean实例的过程分析
从类图可以看出&#xff0c;所有的AutoProxyCreator都间接实现了接口InstantiationAwareBeanPostProcessor。而InstantiationAwareBeanPostProcessor#postProcessAfterInitialization方法提供了一个机会&#xff0c;完成代理对象的创建&#xff0c;并缓存到容器中&#xff0c;供后续使用
BeanNameAutoProxyCreator

使用BeanNameAutoProxyCreator的伪代码

TargetClass target1 &#61; new TargetClass();
TargetClass target2 &#61; new TargetClass();BeanNameAutoProxyCreator autoProxyCreator &#61; new BeanNameAutoProxyCreator();
// 指定目标实例的beanname
autoProxyCreator.setBeanNames("target1", "target2");
// 指定Advice&#xff08;切面逻辑&#xff09;
autoProxyCreator.setInterceptorNames("a interceptor name");// 完成以上配置的BeanNameAutoProxyCreator&#xff0c;注册到IOC容器时&#xff0c;将自动完成对target1、target2两个bean进行织入切面逻辑

总结

总结一下&#xff1a;

  1. 我们了解了spring aop中AOP实体的实现类&#xff1a;JoinPoint、Pointcut、Advice、Advisor&#xff08;Aspect&#xff09;等
  2. 使用ProxyFactory在容器外进行切面逻辑的织入
  3. 使用ProxyFactoryBean在容器管理Advice、Advisor实例bean的基础上&#xff0c;进行切面逻辑的织入
  4. 使用AbstractAutoProxyCreator的实现类&#xff0c;通过一些配置可以实现在容器启动时&#xff0c;自动生成代理类。免去了手动生成代理类的过程

以下内容就是spring aop框架的实现原理&#xff0c;可以看到创建aop的过程相当的繁琐&#xff0c;并且如果使用这种方式来创建代理类&#xff0c;织入切面逻辑的话&#xff0c;存在大量的模板代码。在spring2.0中&#xff0c;使用了一种全新的方法来简化我们开发AOP的流程。我们在下篇文章进行分析吧




推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • importjava.util.ArrayList;publicclassPageIndex{privateintpageSize;每页要显示的行privateintpageNum ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
author-avatar
phper
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有