zoukankan      html  css  js  c++  java
  • 依赖注入草稿

    加载类
    实例化前 -----》InstantiationAwareBeanPostProcessor
    实例化
    mergedBeanDefinitionBeanPostProcessor.applyMergedBeanDefinitionPostProcessors
    找注入点
    实例化后 --》 InstantiationAwareBeanPostProcessor 程序员手动填充属性
    填充属性、 byType、byName
    填充属性后 --》 InstantiationAwareBeanPostProcessors 中的 postProcessPropertyValues 、postProcessProperties 进行依赖注入
    Aware
    初始化前 -- BeanPostProcessor
    初始化
    初始化后 -- BeanPostProcessor

    spring依赖注入的方式:

    1、手动注入 依赖的是set方法
    依赖 set方法底层原理
    依赖 构造函数方法底层原理

    2、 自动注入

    1、xml的自动注入 什么是xml的自动注入
    1、set方法 2、构造方法
    依赖 set方法底层原理
    依赖 构造函数方法底层原理
    判断什么方法是set 方法 : 1、方法名以set开头 2、入参只有一个
    这边判断set方法的代码在哪里呢?利用Java内省里面的代码设置的
    AbstractAutowireCapableBeanFactory#autowireByType
    AbstractAutowireCapableBeanFactory#unsatisfiedNonSimpleProperties
    这个调用链很深一直到Introspector#getTargetPropertyInfo
    下面为代码片段:
    private PropertyDescriptor[] getTargetPropertyInfo() {

        // Check if the bean has its own BeanInfo that will provide
        // explicit information.
        PropertyDescriptor[] explicitProperties = null;
        if (explicitBeanInfo != null) {
            explicitProperties = getPropertyDescriptors(this.explicitBeanInfo);
        }
    
        if (explicitProperties == null && superBeanInfo != null) {
            // We have no explicit BeanInfo properties.  Check with our parent.
            addPropertyDescriptors(getPropertyDescriptors(this.superBeanInfo));
        }
    
        for (int i = 0; i < additionalBeanInfo.length; i++) {
            addPropertyDescriptors(additionalBeanInfo[i].getPropertyDescriptors());
        }
    
        if (explicitProperties != null) {
            // Add the explicit BeanInfo data to our results.
            addPropertyDescriptors(explicitProperties);
    
        } else {
    
            // Apply some reflection to the current class.
    
            // First get an array of all the public methods at this level
            Method methodList[] = getPublicDeclaredMethods(beanClass);
    
            // Now analyze each method.
            for (int i = 0; i < methodList.length; i++) {
                Method method = methodList[i];
                if (method == null) {
                    continue;
                }
                // skip static methods.
                int mods = method.getModifiers();
                if (Modifier.isStatic(mods)) {
                    continue;
                }
                String name = method.getName();
                Class<?>[] argTypes = method.getParameterTypes();
                Class<?> resultType = method.getReturnType();
                int argCount = argTypes.length;
                PropertyDescriptor pd = null;
    
                if (name.length() <= 3 && !name.startsWith(IS_PREFIX)) {
                    // Optimization. Don't bother with invalid propertyNames.
                    continue;
                }
    
                try {
    
                    if (argCount == 0) {
                        if (name.startsWith(GET_PREFIX)) {
                            // Simple getter
                            pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
                        } else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
                            // Boolean getter
                            pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
                        }
                    } else if (argCount == 1) {
                        if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
                            pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);
                        } else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {
                            // Simple setter
                            pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);
                            if (throwsException(method, PropertyVetoException.class)) {
                                pd.setConstrained(true);
                            }
                        }
                    } else if (argCount == 2) {
                            if (void.class.equals(resultType) && int.class.equals(argTypes[0]) && name.startsWith(SET_PREFIX)) {
                            pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, null, method);
                            if (throwsException(method, PropertyVetoException.class)) {
                                pd.setConstrained(true);
                            }
                        }
                    }
                } catch (IntrospectionException ex) {
                    // This happens if a PropertyDescriptor or IndexedPropertyDescriptor
                    // constructor fins that the method violates details of the deisgn
                    // pattern, e.g. by having an empty name, or a getter returning
                    // void , or whatever.
                    pd = null;
                }
    
                if (pd != null) {
                    // If this class or one of its base classes is a PropertyChange
                    // source, then we assume that any properties we discover are "bound".
                    if (propertyChangeSource) {
                        pd.setBound(true);
                    }
                    addPropertyDescriptor(pd);
                }
            }
        }
        processPropertyDescriptors();
    
        // Allocate and populate the result array.
        PropertyDescriptor result[] =
                properties.values().toArray(new PropertyDescriptor[properties.size()]);
    
        // Set the default index.
        if (defaultPropertyName != null) {
            for (int i = 0; i < result.length; i++) {
                if (defaultPropertyName.equals(result[i].getName())) {
                    defaultPropertyIndex = i;
                }
            }
        }
    
        return result;
    }
    

    2、@Autowire 自动注入
    希望spring不去调用set方法

      @Bean 替代<Bean></Bean>
      1、set方法
      2、 属性
      3、 构造方法
      此时就不需要提供属性的set方法
       先byType 再Byname ---> 先属性的类型 ,再属性的名字
    

    3、xml 的byType byName是spring自带的,
    但是@Autowire 是通过BeanPostProcess 实现的,是spring的插件机制

    4、value注解的使用
    5、查找bean的候选者
    DefaultListableBeanFactory#findAutowireCandidates
    DefaultListableBeanFactory#isAutowireCandidate
    6、为啥xml配置的自动注入 byType 获取多个bean之后只能报错?
    为什么因为byType 封装的AutowireByTypeDependencyDescriptor.getDependencyName()==null
    而@Autowire 是先ByType 再ByName
    6、一个对象实例化后从spring容器取出放在bean对象中,
    写出获取它的属性的set方法 https://blog.csdn.net/lw_100831/article/details/42814995
    思路:可以用java的内省获取bean的属性描述器ps(bean的全部属性),若其中属性pd(ps中的一个)和从配置文件解析出的bean的某个属性相等,则Method method=pd.getWriteMethod( ),那么这个method就是那个属性的set方法

    appl
    PropertyDescriptor[] ps = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors();
    for(PropDef propDef : beanDef.getPropertys()){ //beanDef是我们昨天设计出用来存放解析bean的list
       for(PropertyDescriptor pd : ps){
      if(propDef.getName().equals(pd.getName())){//PropDef是我们昨天设计出用来存放bean下面的property的 list
       Method setter = pd.getWriteMethod();//获取属性的setter方法 ,p
       Object value = map.get(proDef.getRef());
       setter.invoke(bean, value);//把引用对象注入到属性
    8、xml 默认是关闭autowire注解
    想要使用@Autowire 有两种方式: 1、切换容器到AnnotationConfigApplicationContext
    2、 spring.xml 注解中开启新增context:annotation-config/

    现在开始讲@Autowire
    9、Spring IoC 依赖注入(三)resolveDependency https://www.cnblogs.com/binarylei/p/12337145.html#3-findautowirecandidates
    认知一下,与依赖查找的相关 API:
    resolveDependency:支持 Optional、延迟注入、懒加载注入、正常注入。
    Spring依赖注入之注入Bean获取详解:https://blog.csdn.net/scjava/article/details/109276166
    doResolveDependency:在依赖查找之前,想办法快速查找,
    如缓存 beanName、@Value 等直接获取注入的值,避免通过类型查找,
    最后才对集合依赖和单一依赖分别进行了处理。
    实际上,无论是集合依赖还是单一依赖查找都是调用 findAutowireCandidates 方法。
    findAutowireCandidates:真正在 Spring IoC 容器中进行依赖查找,
    依赖查找的来源有三:①内部对象 ②托管Bean ③BeanDefinition。
    最后如果无法查找到依赖对象,会进行一些补偿机制,想方设法获取注入的对象,如泛型补偿,自引用补偿。
    isAutowireCandidate:判断候选对象是否可用,
    有三重过滤规则:①bd.autowireCandidate=true -> ②泛型匹配 -> ③@Qualifier。委托给 ContextAnnotationAutowireCandidateResolver。
    泛型匹配的demo:
    https://blog.csdn.net/qingpengshan/article/details/80587452 Spring泛型依赖注入
    https://www.zhihu.com/question/268195272 spring4的泛型依赖注入是什么原理?
    其中函数 isAutowireCandidate 往里面找public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
    if (!super.isAutowireCandidate(bdHolder, descriptor)) {
    // If explicitly false, do not proceed with any other checks...
    return false;
    }
    return checkGenericTypeMatch(bdHolder, descriptor);
    }看到这里有范型检查相关的内容,再往深入找,会看到这段代码if (checkGenerics) {
    // Recursively check each generic
    ResolvableType[] ourGenerics = getGenerics();
    ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics();
    if (ourGenerics.length != typeGenerics.length) {
    return false;
    }
    if (matchedBefore == null) {
    matchedBefore = new IdentityHashMap<>(1);
    }
    matchedBefore.put(this.type, other.type);
    for (int i = 0; i < ourGenerics.length; i++) {
    if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], matchedBefore)) {
    return false;
    }
    }
    }所以 spring 其实是利用反射机制,获取类型的范型的,然后做了比较返回了合适的 bean 进行注入的。

    Spring 注解原理(三)AutowireCandidateResolver:@Qualifier @Value @Autowire @Lazy
    http://t.zoukankan.com/binarylei-p-10428999.html#4-generictypeawareautowirecandidateresolver

    8、自己注入自己就是类似下面
    1、 A a = new A();
    a.a =a;
    9、
    UserService.java
    @Component
    public class UserService {
    @Autowired
    public OrderService orderService;

    public void test(){
    	System.out.println(this.orderService);
    }
    

    }
    OrderService.java
    @Component
    @Scope("prototype")
    public class OrderService {
    }
    Test.java
    public class Test {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    UserService userService = (UserService) applicationContext.getBean("userService");
    userService.test();
    userService.test();
    userService.test();
    }
    }
    com.luban.service.OrderService@2f7298b
    com.luban.service.OrderService@2f7298b
    com.luban.service.OrderService@2f7298b
    当UserService注入的类型是ObjectFactory的时候可以改变成其他结果
    @Component
    public class UserService {
    @Autowired
    public ObjectFactory orderService;

    public void test(){
    	System.out.println(this.orderService.getObject());
    }
    

    }
    com.luban.service.OrderService@4cc451f2
    com.luban.service.OrderService@294425a7
    com.luban.service.OrderService@9f116cc

    1. 当注入的属性带有@Lazy注解,则会注入代理类。只有代理类调用方法的时候才会注入bean (才会执行doResolveDependency方法)
      @Rsource
      对于@Resource:
    2. 如果@Resource注解中指定了name属性,那么则只会根据name属性的值去找bean,如果找不到则报错
    3. 如果@Resource注解没有指定name属性,那么会先判断当前注入点名字(属性名字或方法参数名字)是不是存在Bean,如果存在,则直接根据注入点名字取获取bean,如果不存在,则会走@Autowired注解的逻辑,会根据注入点类型去找Bean
      3、如果

    @Resource 不能注入静态的属性或者方法会抛异常
    @Autowire 不能成功注入静态的属性或者方法,会打印日志
    除了 value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);这行代码,其他在上一篇笔记中已经写了;总结下这个过程就是:
    1.根据注入类型判断如果是ObjectFactory,返回一个ObjectFactory对象,程序员通过ObjectFactory.getObject来获取对象,如果是原型对象,那么每次获取的对象都不一样;也就是说通过一个原型bean对象,通过注入过后,每次执行getObject都是不一样的对象。
    2.如果是Optional对象,然后获取一个Bean对象,封装成Optional对象返回;
    3.如果是一般的注入对象,那么过程如下:
    如果有@Value注解,处理@Value注解;
    否则处理Map、Collection、Array类型的注入,如果是这些类型,那么会从容器获取bean然后封装成Map or Collection or Array类型,然后返回;
    最后调用findAutowireCandidates方法得到根据注入类型找到的所有bean
    a.isAutowireCandidate的验证(默认为true)
    b.泛型的验证;
    c.Qualifier的筛选;
    通过这些验证过后得到了一个Map集合,Map集合就是符合条件的所有bean,如果这个Map只有一条数据那么直接简单处理返回,如果这个Map大于1的时候,就要进行进一步的唯一筛选,筛选过程如下:
    找到有多个bean,这里的处理就比较麻烦一点了
    determineAutowireCandidate确定唯一Bean的三步操作
    1.检查是否有@Primary注解,如果有就取配置了@Primary注解的bean,如果发现有不止一个bean有@Primary,则报错;如果没有这注解,当没调用
    @primary作用在bean定义上面
    当从容器中查找一个bean的时候,如果容器中出现多个Bean候选者时,可以通过primary="true"将当前bean置为首选者,那么查找的时候就会返回主要的候选者,否则将抛出异常。
    2.检查是否有@Priority注解,如果有判断优先级,如果有两个bean的优先级相同则报错;如果没有这注解,当没调用
    @Priority 注解作用在类上面,被@Priority注解的类,其值越小,在单值注入时,越优先选择。
    3.如果前两步还没有确定唯一的bean,那么最后一步就是byName,找到唯一的bean。

    所以找bean的过程是:
    byType->Map等集合处理->自动注入候选者验证->Qualifier的筛选->@Primary->@Priority->byName的过程
    所以依赖注入不仅仅是先byType,再byName,这中间还有很多过程。
    Spring依赖注入之注入Bean获取详解 https://blog.csdn.net/scjava/article/details/109276166

    从源码分析@Qualifier,@Primary,@Priority的候选顺序
    @Primary、@Priority、@Qualifier的用法??https://blog.csdn.net/z69183787/article/details/110638883

    自己注入自己
    定义一个类,类里面提供了一个构造方法,用来设置name属性
    public class UserService {
    private String name;
    public UserService(String name) {
    this.name = name;
    }
    public String getName() {
    return name;
    }
    @Autowired
    private UserService userService;
    public void test() {
    System.out.println(userService.getName());
    }
    }
    然后针对UserService定义两个Bean:
    @Bean
    public UserService userService1() {
    return new UserService("userService1");
    }
    @Bean
    public UserService userService() {
    return new UserService("userService");
    }
    按照正常逻辑来说,对于注入点:
    @Autowired
    private UserService userService;
    会先根据UserService类型去找Bean,找到两个,然后根据属性名字“userService”找到一个beanName为userService的Bean,但是我们直接运行Spring,会发现注入的是“userService1”的那个Bean。
    这是因为Spring中进行了控制,尽量“自己不注入自己”。
    涉及到的代码:
    findAutowireCandidates
    // 对候选bean进行过滤,首先候选者不是自己,然后候选者是支持自动注入给其他bean的
    for (String candidate : candidateNames) { // beanName orderSer1 order2 oser
    // isAutowireCandidate方法中会去判断候选者是否和descriptor匹配
    if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
    addCandidateEntry(result, candidate, descriptor, requiredType);
    }
    }
    // 3. 补偿机制:如果依赖查找无法匹配,怎么办?包含泛型补偿和自身引用补偿两种。
    if (result.isEmpty()) {
    boolean multiple = indicatesMultipleBeans(requiredType);
    // 3.1 fallbackDescriptor: 泛型补偿,实际上是允许注入对象类型的泛型存在无法解析的情况
    DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
    // 3.2 补偿1:不允许自称依赖,但如果是集合依赖,需要过滤非@Qualifier对象。什么场景?
    for (String candidate : candidateNames) {
    if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) &&
    (!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) {
    addCandidateEntry(result, candidate, descriptor, requiredType);
    }
    }
    // 3.3 补偿2:允许自称依赖,但如果是集合依赖,注入的集合依赖中需要过滤自己
    if (result.isEmpty() && !multiple) {
    for (String candidate : candidateNames) {
    if (isSelfReference(beanName, candidate) &&
    (!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
    isAutowireCandidate(candidate, fallbackDescriptor)) {
    addCandidateEntry(result, candidate, descriptor, requiredType);
    }
    }
    }
    }

    spring的销毁:
    Bean的销毁过程

    1. 容器关闭
    2. 发布ContextClosedEvent事件
    3. 调用LifecycleProcessor的onClose方法
    4. 销毁单例Bean
    5. 找出所有DisposableBean(实现了DisposableBean接口的Bean)
    6. 遍历每个DisposableBean
    7. 找出依赖了当前DisposableBean的其他Bean,将这些Bean从单例池中移除掉
    8. 调用DisposableBean的destroy()方法
    9. 找到当前DisposableBean所包含的inner beans,将这些Bean从单例池中移除掉 (inner bean参考https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-inner-beans)
      这里涉及到一个设计模式:适配器模式
      在销毁时,Spring会找出实现了DisposableBean接口的Bean。

    Spring依赖注入之@Resourcce详解&Bean的销毁:
    https://blog.csdn.net/scjava/article/details/109276324
    Bean的销毁
    当容器进行关闭的时候需要对bean和bean工厂以及一系列的容器进行销毁,
    这个时候销毁只是对容器的bean对象进行销毁,不意味着对对象进行销毁;销毁分为两种,
    手动销毁和自动销毁,手动销毁需要调用容器的close方法进行销毁,
    而自动销毁是向jvm注册一个钩子线程,
    当容器进行关闭的时候会自动调用销毁的钩子线程进行销毁,
    比如我们的jvm关闭的时候会自动来调用你的钩子线程来销毁容器的bean

    // Publish shutdown event.向spring容器发布一个事件,表示容器即将关闭
    publishEvent(new ContextClosedEvent(this));
    spring容器的关闭不代表jvm的关闭,如果定义了事件关闭监听器的话 jvm可以收到事件关闭的监听器
    dubbo里面会监听到关闭事件

    public DisposableBeanAdapter(Object bean, String beanName, RootBeanDefinition beanDefinition,
    		List<BeanPostProcessor> postProcessors, @Nullable AccessControlContext acc) {
    
    	Assert.notNull(bean, "Disposable bean must not be null");
    	//销毁方法所在的bean对象
    	this.bean = bean;
    	this.beanName = beanName;
    	//这个属性invokeDisposableBean是表示你的销毁方法   实现了DisposableBean并且bean中不存在destroy方法
    	this.invokeDisposableBean =
    			(this.bean instanceof DisposableBean && !beanDefinition.isExternallyManagedDestroyMethod("destroy"));
    	//是否允许非public的构造或者普通方法执行
    	this.nonPublicAccessAllowed = beanDefinition.isNonPublicAccessAllowed();
    	this.acc = acc;
    	//		https://blog.csdn.net/scjava/article/details/109276324
    	String destroyMethodName = inferDestroyMethodIfNecessary(bean, beanDefinition);
    	// 当前bean没有实现DisposableBean 并且 !"destroy".equals(destroyMethodName)
    	//  bean中不存在destroyMethodName方法
    	// destroyMethodName 不为空
    	if (destroyMethodName != null && !(this.invokeDisposableBean && "destroy".equals(destroyMethodName)) &&
    			!beanDefinition.isExternallyManagedDestroyMethod(destroyMethodName)) {
    		this.destroyMethodName = destroyMethodName;
    		Method destroyMethod = determineDestroyMethod(destroyMethodName);
    		if (destroyMethod == null) {
    			if (beanDefinition.isEnforceDestroyMethod()) {
    				throw new BeanDefinitionValidationException("Could not find a destroy method named '" +
    						destroyMethodName + "' on bean with name '" + beanName + "'");
    			}
    		}
    		else {
    			Class<?>[] paramTypes = destroyMethod.getParameterTypes();
    			if (paramTypes.length > 1) {
    				throw new BeanDefinitionValidationException("Method '" + destroyMethodName + "' of bean '" +
    						beanName + "' has more than one parameter - not supported as destroy method");
    			}
    			else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) {
    				throw new BeanDefinitionValidationException("Method '" + destroyMethodName + "' of bean '" +
    						beanName + "' has a non-boolean parameter - not supported as destroy method");
    			}
    			destroyMethod = ClassUtils.getInterfaceMethodIfPossible(destroyMethod);
    		}
    		this.destroyMethod = destroyMethod;
    	}
    	this.beanPostProcessors = filterPostProcessors(postProcessors, bean);
    }
    private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
    	String destroyMethodName = beanDefinition.getDestroyMethodName();
    	if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
    			(destroyMethodName == null && bean instanceof AutoCloseable)) {
    		// Only perform destroy method inference or Closeable detection
    		// in case of the bean not explicitly implementing DisposableBean
    		if (!(bean instanceof DisposableBean)) {
    			try {
    				return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName();
    			}
    			catch (NoSuchMethodException ex) {
    				try {
    					return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName();
    				}
    				catch (NoSuchMethodException ex2) {
    					// no candidate destroy method found
    				}
    			}
    		}
    		return null;
    	}
    	return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null);
    }
    

    执行顺序:
    @Override
    public void destroy() {
    // bean

    	if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
    		// 执行@PreDestroy
    		for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
    			processor.postProcessBeforeDestruction(this.bean, this.beanName);
    		}
    	}
        //执行实现DisposableBean的destory方法
    	if (this.invokeDisposableBean) {
    		if (logger.isTraceEnabled()) {
    			logger.trace("Invoking destroy() on bean with name '" + this.beanName + "'");
    		}
    		try {
    			if (System.getSecurityManager() != null) {
    				AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
    					((DisposableBean) this.bean).destroy();
    					return null;
    				}, this.acc);
    			}
    			else {
    				((DisposableBean) this.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) {
    		invokeCustomDestroyMethod(this.destroyMethod);
    	}
    	else if (this.destroyMethodName != null) {
    		Method methodToInvoke = determineDestroyMethod(this.destroyMethodName);
    		if (methodToInvoke != null) {
    			invokeCustomDestroyMethod(ClassUtils.getInterfaceMethodIfPossible(methodToInvoke));
    		}
    	}
    }
    

    1、 @preDestory
    2、 实现了DisposableBean接口的Bean的destory方法
    3、1 bean定义中destroyMethod 如xml中的 <bean id="aa" class="" init-method="initialize"destroy-method="cleanup"/>
    当上面bean定义中destroyMethod 为空时候:
    3、2
    3.2.1 bean定义中destroyMethod == null
    实现了DisposableBean接口的Bean的close方法
    3.2.2 bean定义中destroyMethod == "(inferred)"
    取close 方法 ,如果close 方法没有取shutdown 方法

  • 相关阅读:
    查找算法——找到序列中第二大的数(修正版)
    排序算法——堆排序
    排序算法——选择排序
    排序算法——冒泡排序
    排序算法——快速排序
    2013年全国各大著名的IT公司薪资待遇大揭密 给出入职场的民工一点建议
    各大IT公司2012校园招聘笔试面试整理
    九大排序算法总结
    mysql之创建数据库,创建数据表
    各种排序算法汇总
  • 原文地址:https://www.cnblogs.com/tangliMeiMei/p/15291650.html
Copyright © 2011-2022 走看看