• InitializingBean和DisposableBean


    InitializingBean

    记住一点:InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的子类,在初始化bean的时候会执行该方法。

    下面看下简单的例子:(环境是用Spring Boot搭建,直接用SpringtestApplication启动即可

    <bean id="myInitializingBean" class="com.paic.phssp.springtest.init.MyInitializingBean" init-method="testInit"></bean>
    package com.paic.phssp.springtest.init;
    
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    
    /**
     * 继承InitializingBean接口的类,在初始化bean的时候会执行该方法
     */
    //@Component
    public class MyInitializingBean implements InitializingBean {
    
        public MyInitializingBean() {
            System.out.println("MyInitializingBean....");
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            System.out.println("ceshi MyInitializingBean>>>>>>>>>>>>>>>>>>>");
        }
    
        @PostConstruct  //功能上近似init-method,但加载时机不同
        public void test(){
            System.out.println("PostConstruct >>>>>>>>>>>>");
        }
    
        public void testInit(){
            System.out.println("ceshi init-method");
        }
    }

    结果:

    MyInitializingBean....
    PostConstruct >>>>>>>>>>>>
    ceshi MyInitializingBean>>>>>>>>>>>>>>>>>>>
    ceshi init-method

    说明:

    通过上述输出结果,三者的先后顺序也就一目了然了:

    Constructor > @PostConstruct > InitializingBean > init-method

    (1)通过查看spring的加载bean的源码类(AbstractAutowireCapableBeanFactory)可看出其中奥妙:

    AbstractAutowireCapableBeanFactory.invokeInitMethods

    protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd) throws Throwable {
            boolean isInitializingBean = bean instanceof InitializingBean;
            if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
                }
    
                if (System.getSecurityManager() != null) {
                    try {
                        AccessController.doPrivileged(() -> {
                            ((InitializingBean)bean).afterPropertiesSet();
                            return null;
                        }, this.getAccessControlContext());
                    } catch (PrivilegedActionException var6) {
                        throw var6.getException();
                    }
                } else {
                    ((InitializingBean)bean).afterPropertiesSet();
                }
            }
    
            if (mbd != null && bean.getClass() != NullBean.class) {
                String initMethodName = mbd.getInitMethodName();
                if (StringUtils.hasLength(initMethodName) && (!isInitializingBean || !"afterPropertiesSet".equals(initMethodName)) && !mbd.isExternallyManagedInitMethod(initMethodName)) {
                    this.invokeCustomInitMethod(beanName, bean, mbd);
                }
            }
    
        }

    说明:

    a、spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用
    b、实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率相对来说要高点。但是init-method方式消除了对spring的依赖
    c、如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法。
    d、@PostConstruct注解后的方法在BeanPostProcessor前置处理器中就被执行了,所以当然要先于InitializingBean和init-method执行了。
     
    下面看@PostConstruct加载过程,主要看CommonAnnotationBeanPostProcessor.class。看下面UML图:
     

    看下源码:InitDestroyAnnotationBeanPostProcessor.class

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata metadata = this.findLifecycleMetadata(bean.getClass());
    
            try {
            //利用反射,执行注解方法 metadata.invokeInitMethods(bean, beanName);
    return bean; } catch (InvocationTargetException var5) { throw new BeanCreationException(beanName, "Invocation of init method failed", var5.getTargetException()); } catch (Throwable var6) { throw new BeanCreationException(beanName, "Failed to invoke init method", var6); } } private InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata findLifecycleMetadata(Class<?> clazz) { if (this.lifecycleMetadataCache == null) { return this.buildLifecycleMetadata(clazz); } else { InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata metadata = (InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata)this.lifecycleMetadataCache.get(clazz); if (metadata == null) { Map var3 = this.lifecycleMetadataCache; synchronized(this.lifecycleMetadataCache) { metadata = (InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata)this.lifecycleMetadataCache.get(clazz); if (metadata == null) { metadata = this.buildLifecycleMetadata(clazz); this.lifecycleMetadataCache.put(clazz, metadata); } return metadata; } } else { return metadata; } } } private InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata buildLifecycleMetadata(Class<?> clazz) { List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> initMethods = new ArrayList(); List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> destroyMethods = new ArrayList(); Class targetClass = clazz; do { List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> currInitMethods = new ArrayList(); List<InitDestroyAnnotationBeanPostProcessor.LifecycleElement> currDestroyMethods = new ArrayList(); ReflectionUtils.doWithLocalMethods(targetClass, (method) -> {
           //判断是否是指定的注解类型
    if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) { InitDestroyAnnotationBeanPostProcessor.LifecycleElement element = new InitDestroyAnnotationBeanPostProcessor.LifecycleElement(method); currInitMethods.add(element); if (this.logger.isTraceEnabled()) { this.logger.trace("Found init method on class [" + clazz.getName() + "]: " + method); } } if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) { currDestroyMethods.add(new InitDestroyAnnotationBeanPostProcessor.LifecycleElement(method)); if (this.logger.isTraceEnabled()) { this.logger.trace("Found destroy method on class [" + clazz.getName() + "]: " + method); } } }); initMethods.addAll(0, currInitMethods); destroyMethods.addAll(currDestroyMethods); targetClass = targetClass.getSuperclass(); } while(targetClass != null && targetClass != Object.class); return new InitDestroyAnnotationBeanPostProcessor.LifecycleMetadata(clazz, initMethods, destroyMethods); }

    方法:buildLifecycleMetadata(),判断是否是指定的注解类型,而这个属性,在CommonAnnotationBeanPostProcessor.class构造方法中被初始化为PostConstruct。

    public CommonAnnotationBeanPostProcessor() {
            this.setOrder(2147483644);
            this.setInitAnnotationType(PostConstruct.class);
            this.setDestroyAnnotationType(PreDestroy.class);
            this.ignoreResourceType("javax.xml.ws.WebServiceContext");
        }

    那么问题来了,以上只能说明实现了BeanPostProcessor的postProcessBeforeInitialization()方法,不足说明@PostConstruct 先InitializingBean 。

    再看AbstractAutowireCapableBeanFactory.class 的initializeBean()方法。

    protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
            if (System.getSecurityManager() != null) {
                AccessController.doPrivileged(() -> {
                    this.invokeAwareMethods(beanName, bean);
                    return null;
                }, this.getAccessControlContext());
            } else {
                this.invokeAwareMethods(beanName, bean);
            }
    
            Object wrappedBean = bean;
            if (mbd == null || !mbd.isSynthetic()) {
                wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
            }
    
            try {
                this.invokeInitMethods(beanName, wrappedBean, mbd);
            } catch (Throwable var6) {
                throw new BeanCreationException(mbd != null ? mbd.getResourceDescription() : null, beanName, "Invocation of init method failed", var6);
            }
    
            if (mbd == null || !mbd.isSynthetic()) {
                wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
            }
    
            return wrappedBean;
        }

    这下明显了,下面小结下:

    BeanPostProcessor的实现类注册到Spring IOC容器后,对于该Spring IOC容器所创建的每个bean实例在初始化方法(如afterPropertiesSet和任意已声明的init方法)调用前,将会调用BeanPostProcessor中的postProcessBeforeInitialization方法,而在bean实例初始化方法调用完成后,则会调用BeanPostProcessor中的postProcessAfterInitialization方法,整个调用顺序可以简单示意如下:
    --> Spring IOC容器实例化Bean
    --> 调用BeanPostProcessor的postProcessBeforeInitialization方法 (@PostConstruct在此)
    --> 调用bean实例的初始化方法(invokeInitMethods-> InitializingBean->init-method)
    --> 调用BeanPostProcessor的postProcessAfterInitialization方法

    参考:https://blog.csdn.net/zl834205311/article/details/78802584

    DisposableBean
    在Bean生命周期结束前调用destory()方法做一些收尾工作,亦可以使用destory-method。
    前者与Spring耦合高,使用类型强转.方法名(),效率高
    后者耦合低,使用反射,效率相对低
     
    首先看下入口代码结构:

    其中AbstractApplicationContext.refresh()

    @Override
        public void refresh() throws BeansException, IllegalStateException {
            synchronized (this.startupShutdownMonitor) {
                // Prepare this context for refreshing.
                // 为刷新工作做一些当前上下文 context 上的准备工作
                prepareRefresh();
    
                // Tell the subclass to refresh the internal bean factory.
                // ApplicationContext 实现了 BeanFactory 接口,但是并非直接作为 Bean 容器。
                // ApplicationContext 中真正直接作为 Bean 容器的是一个内部Bean工厂 BeanFactory,
                // 通过其方法 getBeanFactory() 得到,此方法在 AbstractApplicationContext 中
                // 被声明为 abstract, 其实现要求由实现子类提供。下面的语句 obtainFreshBeanFactory()
                // 内部就是通过调用 getBeanFactory() 获得这个内部 Bean 工厂的。
                ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    
                // Prepare the bean factory for use in this context.
                // 准备当前上下文使用的Bean容器 BeanFactory,设置其标准上下文特征,比如类加载器等
                // 1. BeanFactory 的类加载器设置为当前上下文的类加载器
                // 2. BeanFactory 的Bean表达式解析器设置为 new StandardBeanExpressionResolver()
                // 3. BeanFactory 增加 BeanPostProcessror new ApplicationListenerDetector(this)
                // 4.三个单例Bean被注册 : environment,systemProperties,systemEnvironment
                prepareBeanFactory(beanFactory);
    
                try {
                    // Allows post-processing of the bean factory in context subclasses.
                    // 在当前上下文使用的Bean容器BeanFactory的标准初始化完成后对其做一些修改。此时
                    // 所有的Bean definition都已经加载但是还没有 Bean 被创建。
                    // 当前上下文使用的Bean容器 BeanFactory 的 post process
                    // 1.当前上下文是 EmbeddedWebApplicationContext 时,
                    // 这个步骤中会对 beanFactory 注册一个 BeanPostProcessor :
                    // WebApplicationContextServletContextAwareProcessor
                    // 2.当前上下文是 AnnotationConfigEmbeddedWebApplicationContext 时,
                    // 如果设置了 basePackages,
                    // 这里会使用 AnnotatedBeanDefinitionReader扫描basePackages;
                    // 如果设置了 annotatedClasses,
                    // 这里会使用 ClassPathBeanDefinitionScanner登记annotatedClasses;
                    postProcessBeanFactory(beanFactory);
    
                    // Invoke factory processors registered as beans in the context.
                    // 在 beanFactory 上调用 BeanFactoryPostProcessors, 
                    // 当前上下文可能会有多个 BeanFactoryPostProcessor 需要应用在 beanFactory 上
                    // ****************************************************************
                    // 这里需要尤其注意区别 BeanFactoryPostProcessor 和 BeanPostProcessor
                    // BeanFactoryPostProcessor : 作用在 Bean定义 上,用来定制修改 Bean定义
                    // BeanPostProcessor :作用在 Bean实例 上,用来修改或者包装 Bean实例
                    // ****************************************************************
                    
                    // 该方法实际上将实现委托出去 : 
                    // PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()
                    invokeBeanFactoryPostProcessors(beanFactory);
    
                    // Register bean processors that intercept bean creation.
                    // 注册 BeanPostProcessor
                    // 该步骤实际工作委托给工具类 PostProcessorRegistrationDelegate 的静态方法
                    // void registerBeanPostProcessors(
                    //   ConfigurableListableBeanFactory beanFactory, 
                    //   AbstractApplicationContext applicationContext)
                    registerBeanPostProcessors(beanFactory);
    
                    // Initialize message source for this context.
                    initMessageSource();
    
                    // Initialize event multicaster for this context.
                    // 初始化当前上下文ApplicationContext要使用的 事件多播器 
                    // ApplicationEventMulticaster applicationEventMulticaster。
                    // 
                    // 如果容器中已经注册类型为ApplicationEventMulticaster并且名称为
                    // applicationEventMulticaster 的Bean,则直接使用;否则,
                    // 新建一个SimpleApplicationEventMulticaster实例并注册到
                    // Bean容器,Bean名称使用 applicationEventMulticaster。
                    initApplicationEventMulticaster();
    
                    // Initialize other special beans in specific context subclasses.
                    // AbstractApplicationContext 中 onRefresh() 方法实现为空,其目的就是
                    // 留给实现子类一个机会做一些上下文相关的刷新工作。在一些特殊Bean初始化时,单
                    // 例 singleton Bean 初始化之前该方法被调用。
                    // 1. 当前上下文是 EmbeddedWebApplicationContext 时,该步骤会创建一个
                    // 内置的 Servlet 容器, 具体参考 EmbeddedWebApplicationContext 的
                    // 方法 void createEmbeddedServletContainer() 
                    onRefresh();
    
                    // Check for listener beans and register them.
                    // 1. 将外部指定到当前上下文的 ApplicationListener 实例关联到上下文多播器
                    //    Q : 什么时候外部给当前上下文指定 ApplicationListener ?
                    //    A : 举例说明,Springboot 应用 SpringApplication 的情况下,是在
                    //        prepareContext()结尾时SpringApplicationRunListeners的
                    //        contextLoaded() 调用中发生的,此时正在广播事件
                    //        ApplicationPreparedEvent 
                    // 2. 将实现了 ApplicationListener 接口的所有 Bean 关联到上下文多播器
                    // 3. 如果上下文属性earlyApplicationEvents中有要通知的事件,广播出去
                    registerListeners();
    
                    // Instantiate all remaining (non-lazy-init) singletons.
                    // 完成 BeanFactory 的初始化工作
                    // 1.BeanFactory冻结所有的Bean定义:不再可以修改或者做post process操作
                    // 2.确保所有的non-lazy-init单例Bean被初始化,也包括FactoryBean
                    // 3.如果所初始化的单例Bean实现了接口SmartInitializingSingleton,调用
                    //   其方法 afterSingletonsInstantiated()
                    finishBeanFactoryInitialization(beanFactory);
    
                    // Last step: publish corresponding event.
                    // 1. 初始化生命周期处理器 LifecycleProcessor, 使用已经存在的Bean或者
                    //    一个新的DefaultLifecycleProcessor实例;
                    // 2. 生命周期处理器 LifecycleProcessor 上传播 refresh 事件
                    // 3. 发布事件 ContextRefreshedEvent
                    // 4. 如果存在 LiveBeansView MBean 的话,关联到当前上下文
                    // 当前上下文是EmbeddedWebApplicationContext的情况下,还会:
                    // 5. 启动EmbeddedServletContainer,比如启动内置 tomcat容器
                    // 6. 发布事件 EmbeddedServletContainerInitializedEvent
                    finishRefresh();
                }
    
                catch (BeansException ex) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("Exception encountered during context initialization - " +
                                "cancelling refresh attempt: " + ex);
                    }
    
                    // Destroy already created singletons to avoid dangling resources.
                    destroyBeans();
    
                    // Reset 'active' flag.
                    cancelRefresh(ex);
    
                    // Propagate exception to caller.
                    throw ex;
                }
    
                finally {
                    // Reset common introspection caches in Spring's core, since we
                    // might not ever need metadata for singleton beans anymore...
                    resetCommonCaches();
                }
            }
        }

    org.springframework.context.support.AbstractApplicationContext#refresh的这一行代码

    protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
       refreshBeanFactory();
       ConfigurableListableBeanFactory beanFactory = getBeanFactory();
       if (logger.isDebugEnabled()) {
          logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
       }
       return beanFactory;
    }

    org.springframework.context.support.AbstractRefreshableApplicationContext#refreshBeanFactory

     protected final void refreshBeanFactory() throws BeansException {
            if (this.hasBeanFactory()) {
                this.destroyBeans();
                this.closeBeanFactory();
            }
    
            try {
                DefaultListableBeanFactory beanFactory = this.createBeanFactory();
                beanFactory.setSerializationId(this.getId());
                this.customizeBeanFactory(beanFactory);
                this.loadBeanDefinitions(beanFactory);
                Object var2 = this.beanFactoryMonitor;
                synchronized(this.beanFactoryMonitor) {
                    this.beanFactory = beanFactory;
                }
            } catch (IOException var5) {
                throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);
            }
        }

    进入到这个方法org.springframework.context.support.AbstractApplicationContext#destroyBeans

    protected void destroyBeans() {
            this.getBeanFactory().destroySingletons();
        }

    org.springframework.beans.factory.support.DefaultListableBeanFactory#destroySingletons

    @Override
       public void destroySingletons() {
          super.destroySingletons();
    //    清除记录的单例beanName的缓存
          this.manualSingletonNames.clear();
          clearByTypeCache();
       }
    org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroySingletons
    @Override
       public void destroySingletons() {
          super.destroySingletons();
    //    清空beanFactory缓存
          this.factoryBeanObjectCache.clear();
       }
    
    public void destroySingletons() {
          if (logger.isDebugEnabled()) {
             logger.debug("Destroying singletons in " + this);
          }
    //    这里使用ConcurrentHashMap本地缓存单例的bean实例,访问次数比较多,提搞并发量
          synchronized (this.singletonObjects) {
             this.singletonsCurrentlyInDestruction = true;
          }
    
          String[] disposableBeanNames;
    //    这里是用LinkedHashMap本地缓存销毁的bean实例
          synchronized (this.disposableBeans) {
             disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet());
          }
          for (int i = disposableBeanNames.length - 1; i >= 0; i--) {
    //       销毁单例的bean
             destroySingleton(disposableBeanNames[i]);
          }
    
          this.containedBeanMap.clear();
          this.dependentBeanMap.clear();
          this.dependenciesForBeanMap.clear();
    
    //    同步清空缓存
          synchronized (this.singletonObjects) {
             this.singletonObjects.clear();
             this.singletonFactories.clear();
             this.earlySingletonObjects.clear();
             this.registeredSingletons.clear();
             this.singletonsCurrentlyInDestruction = false;
          }
       }
    
    public void destroySingleton(String beanName) {
          // Remove a registered singleton of the given name, if any.删除单例的bean,从本地缓存中删除
          removeSingleton(beanName);
    
          // Destroy the corresponding DisposableBean instance.
          DisposableBean disposableBean;
          synchronized (this.disposableBeans) {
    //       从本地缓存中删除
             disposableBean = (DisposableBean) this.disposableBeans.remove(beanName);
          }
    //    bean销毁的逻辑
          destroyBean(beanName, disposableBean);
       }
    
    protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
          // Trigger destruction of dependent beans first... 先触发依赖的bean销毁,从本地缓存中删除
          Set<String> dependencies = this.dependentBeanMap.remove(beanName);
          if (dependencies != null) {
             if (logger.isDebugEnabled()) {
                logger.debug("Retrieved dependent beans for bean '" + beanName + "': " + dependencies);
             }
             for (String dependentBeanName : dependencies) {
    //          这里用了一个递归删除单例bean,当这个bean没有依赖的bean要删除的时候,递归结束
                destroySingleton(dependentBeanName);
             }
          }
    
          // Actually destroy the bean now... 这里开始删除单例bean
          if (bean != null) {
             try {
    //          bean可以实现DisposableBean这个接口,重写父类的bean destory的方法
                bean.destroy();
             }
             catch (Throwable ex) {
                logger.error("Destroy method on bean with name '" + beanName + "' threw an exception", ex);
             }
          }
    
          // Trigger destruction of contained beans...从本地缓存中销毁内部bean
          Set<String> containedBeans = this.containedBeanMap.remove(beanName);
          if (containedBeans != null) {
             for (String containedBeanName : containedBeans) {
    //          这个地方还是递归调用,删除单例bean,当这个bean没有内部bean时递归结束
                destroySingleton(containedBeanName);
             }
          }
    
          // Remove destroyed bean from other beans' dependencies. 从其他bean依赖中删除销毁的bean
          synchronized (this.dependentBeanMap) {
             for (Iterator<Map.Entry<String, Set<String>>> it = this.dependentBeanMap.entrySet().iterator(); it.hasNext();) {
                Map.Entry<String, Set<String>> entry = it.next();
                Set<String> dependenciesToClean = entry.getValue();
                dependenciesToClean.remove(beanName);
                if (dependenciesToClean.isEmpty()) {
                   it.remove();
                }
             }
          }
    
          // Remove destroyed bean's prepared dependency information.删除销毁的bean准备的依赖信息
          this.dependenciesForBeanMap.remove(beanName);
       }
    View Code
    bean可以实现DisposableBean这个接口,重写父类的bean destory的方法
     注意:此处destroyBean将会被字类重写,用适配器模式销毁
    org.springframework.beans.factory.support.DisposableBeanAdapter#destroy
    @Override
       public void destroy() {
    //    执行beanPostProcessors,beanPostProcessors用对对bean的过程进行处理的抽象
          if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
             for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
    //          在bean销毁之前进行一些处理
                processor.postProcessBeforeDestruction(this.bean, this.beanName);
             }
          }
    
          if (this.invokeDisposableBean) {
             if (logger.isDebugEnabled()) {
                logger.debug("Invoking destroy() on bean with name '" + this.beanName + "'");
             }
             try {
                if (System.getSecurityManager() != null) {
                   AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                      ((DisposableBean) bean).destroy();
                      return null;
                   }, acc);
                }
                else {
    //             bean实现DisposableBean接口的方式,注解调用子类destroy方法
                   ((DisposableBean) bean).destroy();
                }
             }
             catch (Throwable ex) {
                String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'";
                if (logger.isDebugEnabled()) {
                   logger.warn(msg, ex);
                }
                else {
                   logger.warn(msg + ": " + ex);
                }
             }
          }
    
          if (this.destroyMethod != null) {
    //       执行bean定义中指定的bean销毁方法
             invokeCustomDestroyMethod(this.destroyMethod);
          }
          else if (this.destroyMethodName != null) {
             Method methodToCall = determineDestroyMethod(this.destroyMethodName);
             if (methodToCall != null) {
                invokeCustomDestroyMethod(methodToCall);
             }
          }
       }
    View Code

    具体结构如下:

    参考:
    https://my.oschina.net/u/3775437/blog/1810419
     
     
     
     
  • 相关阅读:
    jQuery Mobile 总结
    妙味,结构化模块化 整站开发my100du
    详解使用icomoon生成字体图标的方法并应用
    Vue.js搭建路由报错 router.map is not a function,Cannot read property ‘component’ of undefined
    jquery 最全知识点图示
    图解Js event对象offsetX, clientX, pageX, screenX, layerX, x区别
    Oracle存储过程及函数的练习题
    SQL中IS NOT NULL与!=NULL的区别
    mysql字符集和排序规则
    一个web项目web.xml的配置中<context-param>配置作用
  • 原文地址:https://www.cnblogs.com/xiaozhuanfeng/p/10415794.html
Copyright © 2020-2023  润新知