点击关注公众号,实用技术文章及时了解

来源:blog.csdn.net/z69183787/article/details/108902510

1. 示例

先来看一个例子,看看什么是构造器注入。

这里我写了一个类,分别有两个构造器,一个是注入一个Bean的构造器,一个是注入两个Bean的构造器:

public class ConstructorAutowiredTest {private User user;private Role role;public ConstructorAutowiredTest() {}public ConstructorAutowiredTest(User user) {this.user = user;}public ConstructorAutowiredTest(User user, Role role) {this.user = user;this.role = role;}public void test(){System.out.println("user: "+user);System.out.println("role: "+role);}
}

Model类User与Role我就不贴代码了,分别是有两个变量,一个id,一个name。

然后就是Spring的配置文件context.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsd "><!-- 使Spring关注Annotation --><context:annotation-config/><bean class="com.mytest.demo.model.Role" id="role"><property name="name" value="testRole"/><property name="id" value="2"/></bean><bean class="com.mytest.demo.model.User" id="user"><property name="id" value="1"/><property name="name" value="testUser"/></bean><bean class="com.mytest.demo.autowired.ConstructorAutowiredTest" id="test"/></beans>

注意,如果需要使用构造器注入,需要 <context:annotation-config /> 此自定义标签开启(关于自定义标签,在本人往期的Spring系列中有详细介绍),具体作用后面再作分析。

那么,该类三个构造器,Spring会使用哪个构造器初始化ConstructorAutowiredTest这个Bean呢?写个测试便知:

public class TestBeanAutowiredConstructor {public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();context.setConfigLocation("context.xml");context.refresh();ConstructorAutowiredTest test = (ConstructorAutowiredTest) context.getBean("test");test.test();}
}

执行test方法,控制台打印:

从这里我们可以看出来,此时三个构造器中Spring使用了无参构造器。

我们换一个方式,将无参构造器注释掉,看看Spring到底会使用哪个构造器呢?同样执行test方法测试,控制台打印:

此时控制台报错,大致意思是Bean的实例化失败了,没有无参的构造器方法调用。此时有个疑问,明明构造器中的参数都是Bean,为什么不能初始化,一定要使用无参的构造器呢?是否是因为有两个构造器的原因?此时我们再注释掉任意一个构造函数,使测试类只有一个带参构造函数:

//    public ConstructorAutowiredTest() {
//    }public ConstructorAutowiredTest(User user) {this.user = user;}//    public ConstructorAutowiredTest(User user, Role role) {
//        this.user = user;
//        this.role = role;
//    }

再次运行测试类,控制台打印:

如果是注释掉第二个构造函数,则结果是两个对象都有。从这里我们可以看出,如果只有一个构造器,且参数为IOC容器中的Bean,将会执行自动注入。这里又有一个疑问,这也太不科学了吧,强制用户一定只能写一个构造器?这时我们猜想@Autowired注解是否能解决这种问题?来试试吧。我们将构造器全部解除注释,将第三个构造器打上@Autowired注解:

public ConstructorAutowiredTest() {
}public ConstructorAutowiredTest(User user) {this.user = user;
}@Autowired
public ConstructorAutowiredTest(User user, Role role) {this.user = user;this.role = role;
}

运行测试,控制台打印:

不出所料,@Autowired注解可以解决这种问题,此时Spring将使用有注解的构造函数进行Bean的初始化。那么,如果有两个@Autowired注解呢?结果肯定是报错,因为@Autowired的默认属性required是为true的,也就是说两个required=true的构造器,Spring不知道使用哪一个。但如果是这样写的话:

public ConstructorAutowiredTest() {
}@Autowired(required = false)
public ConstructorAutowiredTest(User user) {this.user = user;
}@Autowired(required = false)
public ConstructorAutowiredTest(User user, Role role) {this.user = user;this.role = role;
}

结果是怎样的呢?看看控制台打印:

使用参数最多的那一个构造器来初始化Bean。又如果两个有参构造器顺序调换又是怎样的呢?一个required为false一个为true,结果又是怎样的呢?这里直接给出答案,顺序调换依然使用多参数构造器,并且required只要有一个true就会报错。有兴趣的读者可以自己试试,下面将深入源码分析构造器注入的过程,相信上述所有疑问都能得到解答。

疑问点小结

从现象看本质,我们从上面的例子中,大致可以得到以下几个疑问:

  • 为什么写三个构造器(含有无参构造器),并且没有@Autowired注解,Spring总是使用无参构造器实例化Bean?

  • 为什么注释掉两个构造器,留下一个有参构造器,并且没有@Autowired注解,Spring将会使用构造器注入Bean的方式初始化Bean?

  • 为什么写三个构造器,并且在其中一个构造器上打上**@Autowired注解,就可以正常注入构造器?并且两个@Autowired注解就会报错**,一定需要在所有@Autowired中的required都加上false即可正常初始化等等?

或许有一些没有提到的疑问点,但大致就这么多吧,举多了也没用,因为在下面深入源码的分析中读者若是可以理解,关于此类的一系列问题都将可以自己思考得出结果,得到可以举一反三的能力。

2. 依赖注入伊始

在开头,我们有提到,如果需要构造器注入的功能的话,我们需要在xml配置中写下这样一段代码:

<!-- 使Spring关注Annotation -->
<context:annotation-config/>

如果有看过本人自定义标签或是有自定义标签的基础的读者,第一反应应该是先看自定义标签的context对应的命名空间是哪个:

xmlns:context="http://www.springframework.org/schema/context"

我们全局搜索此命名空间(http后需要加一个斜杆符号\)得到一个spring.handlers文件:

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler

这里说明了此命名空间对应的NamespaceHandler,进入此类看看其init方法:

public void init() {registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}

我们的自定义标签是叫作 “annotation-config" ,所以对应的解析器是AnnotationConfigBeanDefinitionParser这个类,进入这个类的parse方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {Object source = parserContext.extractSource(element);// Obtain bean definitions for all relevant BeanPostProcessors.//定义一系列BeanDefinition,放入一个Set集合中Set<BeanDefinitionHolder> processorDefinitions =AnnotationConfigUtils.registerAnnotationConfigProcessors(parserContext.getRegistry(), source);//注册一系列BeanDefinition,此处略过..
}

这里只需要关注registerAnnotationConfigProcessors方法,看看到底需要注册哪些Bean:

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {//将registry对象转型成beanFactroyDefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);//判断容器中是否已经存在名为AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME的Beanif (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {//若是不存在,创建一个RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));}//中途还添加了一系列BeanDefinition,不是本文讨论范围,略过...return beanDefs;
}

到这里我们可以知道,此自定义标签注册了一个AutowiredAnnotationBeanPostProcessor类的Bean到IOC容器。那么此类是干什么用的呢?

3. 初始化Bean

我们将思路转到IOC容器初始化Bean的流程中来,在getBean方法获取一个Bean的时候,IOC容器才开始将Bean进行初始化,此时会先实例化一个Bean,然后再对Bean进行依赖注入,然后执行一系列初始化的方法,完成Bean的整个初始化过程。而本文讨论的构造器注入,则是在实例化Bean的过程中进行的。在AbstractAutowireCapableBeanFactory类中的doCreateBean方法获取Bean的开始,将调用createBeanInstance方法进行Bean的实例化(选择Bean使用哪个构造器实例化):

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {//略过一些与本文无关的其他实例化的方式的代码...// Candidate constructors for autowiring?// 查找是否存在候选的自动注入构造器Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {//使用注入的方式调用构造器实例化return autowireConstructor(beanName, mbd, ctors, args);}// No special handling: simply use no-arg constructor.//使用无参构造器实例化(利用反射newInstance)return instantiateBean(beanName, mbd);
}

到这里我们可以知道,determineConstructorsFromBeanPostProcessors方法将选择是否有适合的自动注入构造器,如果没有,将使用无参构造器实例化,关键就在这个方法中,是如何判断哪些构造器使用自动注入的呢:

protected Constructor<?>[] determineConstructorsFromBeanPostProcessors(@Nullable Class<?> beanClass, String beanName)throws BeansException {//容器中若存在InstantiationAwareBeanPostProcessorsif (beanClass != null && hasInstantiationAwareBeanPostProcessors()) {//遍历此BeanPostProcessorsfor (BeanPostProcessor bp : getBeanPostProcessors()) {//只使用SmartInstantiationAwareBeanPostProcessor类型if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;Constructor<?>[] ctors = ibp.determineCandidateConstructors(beanClass, beanName);if (ctors != null) {return ctors;}}}}return null;
}

这段代码告诉我们,这里会使用SmartInstantiationAwareBeanPostProcessor类型的BeanPostProcessor进行判断,我们回顾一下上面的依赖注入伊始的时候我们说的自定义标签注册的类的结构:

有看过本人关于Sprng扩展篇的文章或是有Spring扩展点基础的读者,应该可以知道,若是注册一个BeanPostProcessor到IOC容器中,在AbstractApplicationContext中的refresh方法会对这些类型的Bean进行处理,存放在一个集合,此时getBeanPostProcessors方法就可以获取到所有BeanPostProcessor集合,遍历集合,便可以调用到我们自定义标签中注册的这个类型的Bean。

当然,SmartInstantiationAwareBeanPostProcessor类型的Bean有很多,但依赖注入是使用上述这个Bean来完成的。回到主线,下面会调用它的determineCandidateConstructors方法进行查找对应构造器(核心方法):

public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)throws BeanCreationException {//略..// Quick check on the concurrent map first, with minimal locking.//先查找缓存Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);//若是缓存中没有if (candidateConstructors == null) {// Fully synchronized resolution now...//同步此方法synchronized (this.candidateConstructorsCache) {candidateConstructors = this.candidateConstructorsCache.get(beanClass);//双重判断,避免多线程并发问题if (candidateConstructors == null) {Constructor<?>[] rawCandidates;try {//获取此Bean的所有构造器rawCandidates = beanClass.getDeclaredConstructors();}catch (Throwable ex) {throw new BeanCreationException(beanName,"Resolution of declared constructors on bean Class [" + beanClass.getName() +"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);}//最终适用的构造器集合List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);//存放依赖注入的required=true的构造器Constructor<?> requiredConstructor = null;//存放默认构造器Constructor<?> defaultConstructor = null;Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);int nonSyntheticConstructors = 0;for (Constructor<?> candidate : rawCandidates) {if (!candidate.isSynthetic()) {nonSyntheticConstructors++;}else if (primaryConstructor != null) {continue;}//查找当前构造器上的注解AnnotationAttributes ann = findAutowiredAnnotation(candidate);//若没有注解if (ann == null) {Class<?> userClass = ClassUtils.getUserClass(beanClass);if (userClass != beanClass) {try {Constructor<?> superCtor =userClass.getDeclaredConstructor(candidate.getParameterTypes());ann = findAutowiredAnnotation(superCtor);}catch (NoSuchMethodException ex) {// Simply proceed, no equivalent superclass constructor found...}}}//若有注解if (ann != null) {//已经存在一个required=true的构造器了,抛出异常if (requiredConstructor != null) {throw new BeanCreationException(beanName,"Invalid autowire-marked constructor: " + candidate +". Found constructor with 'required' Autowired annotation already: " +requiredConstructor);}//判断此注解上的required属性boolean required = determineRequiredStatus(ann);//若为trueif (required) {if (!candidates.isEmpty()) {throw new BeanCreationException(beanName,"Invalid autowire-marked constructors: " + candidates +". Found constructor with 'required' Autowired annotation: " +candidate);}//将当前构造器加入requiredConstructor集合requiredConstructor = candidate;}//加入适用的构造器集合中candidates.add(candidate);}//如果该构造函数上没有注解,再判断构造函数上的参数个数是否为0else if (candidate.getParameterCount() == 0) {//如果没有参数,加入defaultConstructor集合defaultConstructor = candidate;}}//适用的构造器集合若不为空if (!candidates.isEmpty()) {// Add default constructor to list of optional constructors, as fallback.//若没有required=true的构造器if (requiredConstructor == null) {if (defaultConstructor != null) {//将defaultConstructor集合的构造器加入适用构造器集合candidates.add(defaultConstructor);}else if (candidates.size() == 1 && logger.isInfoEnabled()) {logger.info("Inconsistent constructor declaration on bean with name '" + beanName +"': single autowire-marked constructor flagged as optional - " +"this constructor is effectively required since there is no " +"default constructor to fall back to: " + candidates.get(0));}}//将适用构造器集合赋值给将要返回的构造器集合candidateConstructors = candidates.toArray(new Constructor<?>[0]);}//如果适用的构造器集合为空,且Bean只有一个构造器并且此构造器参数数量大于0else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {//就使用此构造器来初始化candidateConstructors = new Constructor<?>[] {rawCandidates[0]};}//如果构造器有两个,且默认构造器不为空else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {//使用默认构造器返回candidateConstructors = new Constructor<?>[] {primaryConstructor, defaultConstructor};}else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {candidateConstructors = new Constructor<?>[] {primaryConstructor};}else {//上述都不符合的话,只能返回一个空集合了candidateConstructors = new Constructor<?>[0];}//放入缓存,方便下一次调用this.candidateConstructorsCache.put(beanClass, candidateConstructors);}}}//返回candidateConstructors集合,若为空集合返回nullreturn (candidateConstructors.length > 0 ? candidateConstructors : null);
}

从这段核心代码我们可以看出几个要点:

在没有@Autowired注解的情况下:

  • 无参构造器将直接加入defaultConstructor集合中。

  • 在构造器数量只有一个且有参数时,此唯一有参构造器将加入candidateConstructors集合中。

  • 在构造器数量有两个的时候,并且存在无参构造器,将defaultConstructor(第一条的无参构造器)放入candidateConstructors集合中。

  • 在构造器数量大于两个,并且存在无参构造器的情况下,将返回一个空的candidateConstructors集合,也就是没有找到构造器。

在有@Autowired注解的情况下:

  • 判断required属性:

    • true:先判断requiredConstructor集合是否为空,若不为空则代表之前已经有一个required=true的构造器了,两个true将抛出异常,再判断candidates集合是否为空,若不为空则表示之前已经有一个打了注解的构造器,此时required又是true,抛出异常。若两者都为空将放入requiredConstructor集合中,再放入candidates集合中。

    • false:直接放入candidates集合中。

  • 判断requiredConstructor集合是否为空(是否存在required=true的构造器),若没有,将默认构造器也放入candidates集合中。

  • 最后将上述candidates赋值给最终返回的candidateConstructors集合。

4.总结

综上所述,我们可以回答开篇疑问点小结所总结的一系列问题了:

1.为什么写三个构造器(含有无参构造器),并且没有@Autowired注解,Spring总是使用无参构造器实例化Bean?

答:参照没有注解的处理方式:若构造器只有两个,且存在无参构造器,将直接使用无参构造器初始化。若大于两个构造器,将返回一个空集合,也就是没有找到合适的构造器,那么参照第三节初始化Bean的第一段代码createBeanInstance方法的末尾,将会使用无参构造器进行实例化。这也就解答了为什么没有注解,Spring总是会使用无参的构造器进行实例化Bean,并且此时若没有无参构造器会抛出异常,实例化Bean失败。

2.为什么注释掉两个构造器,留下一个有参构造器,并且没有@Autowired注解,Spring将会使用构造器注入Bean的方式初始化Bean?

答:参照没有注解的处理方式:构造器只有一个且有参数时,将会把此构造器作为适用的构造器返回出去,使用此构造器进行实例化,参数自然会从IOC中获取Bean进行注入。

3.为什么写三个构造器,并且在其中一个构造器上打上@Autowired注解,就可以正常注入构造器?

答:参照有注解的处理方式:在最后判断candidates适用的构造器集合是否为空时,若有注解,此集合当然不为空,且required=true,也不会将默认构造器集合defaultConstructor加入candidates集合中,最终返回的是candidates集合的数据,也就是这唯一一个打了注解的构造器,所以最终使用此打了注解的构造器进行实例化。

4.两个@Autowired注解就会报错,一定需要在所有@Autowired中的required都加上false即可正常初始化?

答:参照有注解的处理方式:当打了两个@Autowired注解,也就是两个required都为true,将会抛出异常,若是一个为true,一个为false,也将会抛出异常,无论顺序,因为有两层的判断,一个是requiredConstructor集合是否为空的判断,一个是candidates集合为空的判断,若两个构造器的required属性都为false,不会进行上述判断,直接放入candidates集合中,并且在下面的判断中会将defaultConstructor加入到candidates集合中,也就是candidates集合有三个构造器,作为结果返回。

至于第四条结论,返回的构造器若有三个,Spring将如何判断使用哪一个构造器呢?在后面Spring会遍历三个构造器,依次判断参数是否是Spring的Bean(是否被IOC容器管理),若参数不是Bean,将跳过判断下一个构造器,也就是说,例如上述两个参数的构造器其中一个参数不是Bean,将判断一个参数的构造器,若此参数是Bean,使用一个参数的构造器实例化,若此参数不是Bean,将使用无参构造器实例化。

也就是说,若使用@Autowired注解进行构造器注入,required属性都设置为false的话,将避免无Bean注入的异常,使用无参构造器正常实例化。若两个参数都是Bean,则就直接使用两个参数的构造器进行实例化并获取对应Bean注入构造器。

在这里最后说一点,从上面可以看出,若想使用构造器注入功能,最好将要注入的构造器都打上@Autowired注解(若有多个需要注入的构造器,将所有@Autowired中required属性都设置为false),若有多个构造器,只有一个构造器需要注入,将这个构造器打上@Autowired注解即可,不用设置required属性。

如果不打注解也是可以使用构造器注入功能的,但构造器数量只能为1,且代码可读性较差,读代码的人并不知道你这里使用了构造器注入的方式,所以这里我建议若使用构造器注入打上@Autowired注解会比较好一点。

推荐

主流Java进阶技术(学习资料分享)

Java面试题宝典

加入Spring技术开发社区

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

深度分析Spring中的构造器注入相关推荐

  1. JavaEE开发之Spring中的依赖注入与AOP编程

    上篇博客我们系统的聊了<JavaEE开发之基于Eclipse的环境搭建以及Maven Web App的创建>,并在之前的博客中我们聊了依赖注入的相关东西,并且使用Objective-C的R ...

  2. spring中的依赖注入——构造函数注入、set方法注入( 更常用的方式)、复杂类型的注入/集合类型的注入

    spring中的依赖注入 依赖注入: Dependency Injection IOC的作用:降低程序间的耦合(依赖关系) 依赖关系的管理:以后都交给spring来维护.在当前类需要用到其他类的对象, ...

  3. java手工注入bean_java相关:Spring中如何动态注入Bean实例教程

    java相关:Spring中如何动态注入Bean实例教程 发布于 2020-3-8| 复制链接 摘记: 前言在Spring中提供了非常多的方式注入实例,但是由于在初始化顺序的不同,基于标注的注入方式, ...

  4. Spring源码深度解析(郝佳)-学习-构造器注入

    本文主要是Spring源码有一定基础的小伙伴而言的,因为这里我只想讲一下,Spring对于构造器的注入参数是如何解析,不同参数个数构造器. 相同参数个数,不同参数类型. Spring是如何选择的. 1 ...

  5. Spring为什么建议构造器注入?

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:https://juejin.cn/post/6844904056230690824 前言 本章的内容主要是想探讨我们在进 ...

  6. junit依赖_3、Spring 中的依赖注入(DI),你都知道多少?

    0. 前言 Spring 中,用来组成应用程序的主体以及由 Spring IoC 容器所管理的对象叫做 Bean.简而言之,Bean 就是由 IoC 容器来进行初始化.装配和管理的对象. Bean 的 ...

  7. 这篇文章,我们来谈一谈Spring中的属性注入

    本系列文章: 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configuration注解? 谈谈Spring中的对象跟Bean,你知道Spring怎么创 ...

  8. Spring 中的依赖注入

    依赖注入 当某个 java 实例需要另一个 java 实例的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例 在 spring 中,创建被调用者的工作不再由调用者来完成,因此称为控制反 ...

  9. Spring中bean的注入方式

    平常的Java开发中,程序员在某个类中需要依赖其它类的方法. 通常是new一个依赖类的实例再调用该实例的方法,这种开发存在的问题是new的类实例不好统一管理. Spring提出了依赖注入的思想,即依赖 ...

最新文章

  1. css的background属性
  2. 图灵测试70载,回顾对话机器人的经典实践和最新进展
  3. 数据结构实验:一元多项式计算器
  4. 链路聚合_配置EthTrunk链路聚合
  5. linux: sort用法
  6. 2016年度最受欢迎中国开源软件评选
  7. FabFilter Total Bundle 2021 for Mac - 经典效果器合集(2022版)
  8. 数学分析第四版上册70页14题
  9. 计算机软件编程应聘ppt,[计算机软件及应用]单片机c编程.ppt
  10. HDU-6638 Snowy Smile 区间最大子段和
  11. 用C语言恶搞你的好朋友strcmp()
  12. 巴别时代php面试题,巴别时代遇到的函数
  13. SM2 加解密注意事项
  14. mysql pdo 端口_链接Mysql的api mysqli和pdo
  15. 2022微软实习面经 | 关于实习面试的所有问题,都能在这里找到答案
  16. 我要的仅此而已:伤感QQ心情日志
  17. pandas数据分析读书笔记(四)
  18. 推荐一些非常好用的网盘搜索神器
  19. 选择结构——判断3或7的倍数
  20. PHP如何去制作一个许愿墙,php许愿墙开发视频教程,php许愿墙源码

热门文章

  1. 别人家的公司!顺丰将向员工提供3亿贷款,无利息无抵押无担保
  2. 腾讯牵手数十家合作伙伴发起“光合计划” 推动“百千万”三大目标落地
  3. 9月26日发布?一加7T系列被“扒光”:硬核到没朋友
  4. 2019年新iPhone假机模曝光 越看越不顺眼
  5. 宁德时代811电芯初现真容 搭配宝马X1混动汽车能量密度提升近6成
  6. 为提升管理效率 蔚来汽车美国办公室裁员70名
  7. 全志R40 UBOOT 2014.07【原创】
  8. python字典和集合对象可以进行索引操作_python字典和列表的高级应用
  9. 查询前10条_98条铁路!2021年底前计划开工建设铁路进度一览(10月8日更新)
  10. Golang实践录:生成版本号和编译时间