IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring源码解析(22)之bean创建流程之FactoryBean -> 正文阅读

[Java知识库]Spring源码解析(22)之bean创建流程之FactoryBean

一、前言

????????在上一节我们主要说了Spring在实例化bean前做的一些工作,主要是注册转换器以及属性解析器并且会冻结目前已经注册的BeandDefinition。

	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
		// 1. 初始化此上下文的转换服务,用来自定义将Spring中的某个Bean的属性从一个类型转换到另外一个类型.
		//    判断Bean工厂中是否存在名称为conversionService的转换服务bean,如果存在而且类型为ConversionService,则获取该Bean实例,并将其设置到BeanFactory中
		/**
		 * 例如:
		 * (1)有如下的javaBean:
		 * public class Person {
		 *     public String name;
		 *     public Date birthday;
		 *     ...
		 * }
		 * (2)有如下的xml配置:
		 * <bean name="person" class="com.wb.test.Person">
		 *     <property name="name" value="wangbing"/>
		 *     <property name="birthday" value="1999-03-03"/>
		 * </bean>
		 * (3)有如下的测试类:
		 * ApplicationContext acx = new ClasspathXmlApplicationContext("test.xml");
		 * Person person = (Person) acx.getBean("person");
		 * System.out.println(person.name);
		 * System.out.println(person.birthday); // 改行会报错,提示字符串类型不能转换为日期类型
		 *
		 * (4)可以通过定义如下名称的bean,将某种类型的属性值转换为另外一种类型.
		 * <bean name="conversionService" class="com.wb.test.MyConversionService" />
		 * public class MyConversionService implements ConversionService {
		 *    // 实现是否能转换以及具体转换的方法。
		 *	  public boolean canConvert(Class<?> sourceType, Class<?> targetType) {}
		 *	  public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {}
		 *    public <T> T convert(Object source, Class<T> targetType) {}
		 *    // 可以在该方法中实现转换逻辑。如果源类型sourceType是String类型的话,将其转换为Date类型返回。
		 *    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {}
		 * }
		 */
		// 2.在Spring中,如果需要配置自定义的转换器,还可以直接利用Spring提供的ConversionServiceFactoryBean来完成。自己只需要实现具体的转换逻辑即可
		/**
		 * (1)配置conversionService对应的工厂Bean:
		 * <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
		 *     <property name="converters">
		 *			<bean class="com.wb.test.MyConverter"/>
		 *     </property>
		 * </bean>
		 * (2)然后自己去实现MyConverter即可:
		 *  public class MyConverter implements Converter<String,Date> {
		 *        @Override
		 *        public Date convert(String source) {
		 *			DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
		 *			try {
		 *				return format.parse((String) source);
		 *            } catch (ParseException e) {
		 *				e.printStackTrace();
		 *            }
		 *			return null;
		 *        }
		 *    }
		 */
		if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME)
				&& beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
			beanFactory.setConversionService(beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
		}
		/**
		 * 如果beanFactory之前没有注册嵌入值解析器,则注册默认的嵌入值解析器,
		 *  主要用于注解属性值的解析例如:@Value("${app.name}")。
		 */
		// 值解析器设置的地方:在调用invokeBeanfactoryPostProcessor方法的时候,通过PropertySourcesPlaceholderConfigurer的后置处理方法设置进去的
		if (!beanFactory.hasEmbeddedValueResolver()) {
			// 调用resolvePlaceholders方法解析strVal对应的值
			beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
		}
		/**
		 * 初始化所有实现了LoadTimeWeaverAware接口的子类,用于类在加载进入jvm之前,动态增强类
		 *  这特别适用于Spring的JPA支持,其中load-time weaving加载织入对JPA类转换非常必要
		 */
		String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
		for (String weaverAwareName : weaverAwareNames) {
			getBean(weaverAwareName);
		}

		// 停止使用临时的类加载器.
		beanFactory.setTempClassLoader(null);

		// 缓存(冻结)所有的BeanName(注册的bean定义不会被修改或进一步做处理了,因为下面马上要创建Bean的实例对象了)
		beanFactory.freezeConfiguration();

		// 初始化所有的单实例Bean,包括创建单实例bean的全部过程
		beanFactory.preInstantiateSingletons();
	}

????????他真正去做实例化bean的还是在preInstantiateSingletons()方法中,接下来我们来分析一下这个方法的源码。

	@Override
	public void preInstantiateSingletons() throws BeansException {
		if (logger.isTraceEnabled()) {
			logger.trace("Pre-instantiating singletons in " + this);
		}

		// 创建BeanDefinitionNames的副本BeanNames用于后续的遍历,以允许init等方法注册新的bean定义.
		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

		// 遍历所有的beanNames,触发所有非懒加载单例bean的初始化,即:创建所有的单实例Bean
		for (String beanName : beanNames) {
			// 获取beanName对应的MergedBeanDefinition.在实例化之前将所有的beanDefiniton对象在转换成RootBeanDefinition,进行缓存,后续在需要马上实例化的时候直接获取定义信息,而定义信息中
			// 如果包含了父类就需要先实例化父类。
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
			// 如果bd对应的Bean实例满足:(不是抽象类 && 是单例 && 不是懒加载)
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
				// 判断BeanName对应的Bean实例是否是FactoryBean.
				/**
				 * BeanFactory 与FactoryBean的区别
				 * 相同点:都是用来创建bean对象的
				 * 不同点:
				 * 如果使用了BeanFactory那么就必须要严格遵循SpringBean的生命周期接口,例如从实例化 ——>初始化等等一系列方法,此流程非常的负责且麻烦
				 * 如果使用FactotyBean则创建bean就更加方便简单,不需要遵循spring的生命周期流程,
				 * 他主要有getObject():直接返回一个对象,isSingleton():判断是否是单例,getObjectTye():返回需要返回对象的类型三个方法。
				 */
				if (isFactoryBean(beanName)) {
					// 通过beanName获取FactoryBean的实例,factoryBean的名称是:"&" + beanName
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
					if (bean instanceof FactoryBean) {
						final FactoryBean<?> factory = (FactoryBean<?>) bean;
						boolean isEagerInit;
						// 判断这个FactoryBean是否需要紧急初始化.
						// System.getSecurityManager()方法是获取系统权限管理器,Java为了防止恶意代码执行(修改,删除操作系统文件),做了权限管理,
						//   默认的安全管理器配置文件是: $JAVA_HOME/jre/lib/security/java.policy
						/**
						 * 在做访问控制决定时,如果遇到通过调用不带上下文参数(请参阅下文,以获取关于上下文参数的信息)的 doPrivileged 标记为“特权”的调用方,
						 *  则 checkPermission 方法将停止检查。如果该调用方的域具有指定的权限,则不进行进一步检查,并且 checkPermission 正常返回,
						 *  指示允许所请求的访问。如果该域不具有指定的权限,则通常抛出异常。
						 *
						 *
						 *  AccessController.doPrivileged()方法的例子:
						 *  假设有这样一种情况:A程序想在 C:\\Users\\Jack\\Desktop\\test1? 这个目录中新建一个文件,但是它没有相应的权限,
						 *   但是它引用了另外一个Jar包B,刚好B有权限在C:\\Users\\Jack\\Desktop\\test1目录中新建文件,
						 *   还有更巧的是B在新建文件的时候采用的是AccessController.doPrivileged方法进行的,这种情况下,A就可以调用B的创建文件的方法进行创建文件了。
						 */
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
											((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							// 如果需要紧急初始化,则通过beanName获取Bean的实例.
							getBean(beanName);
						}
					}
				}
				else {
					// 如果BeanName对应的Bean实例不是FactoryBean,则通过BeanName去获取Bean实例.
					getBean(beanName);
				}
			}
		}

		/**
		 * 上一步for循环中已经创建完了所有的单实例Bean,这个for循环中,会拿出所有的单实例Bean,
		 *   然后遍历,判断单实例bean是否实现了SmartInitializingSingleton接口,如果实现了该接口,
		 *   则调用单实例Bean的afterSingletonsInstantiated方法
		 */
		for (String beanName : beanNames) {
			// 获取beanName对应的bean实例
			Object singletonInstance = getSingleton(beanName);
			// 判断当前的bean是否实现了SmartInitializingSingleton接口.
			if (singletonInstance instanceof SmartInitializingSingleton) {
				final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
				// 触发SmartInitializingSingleton实现类的afterSingletonInstantiated方法.
				if (System.getSecurityManager() != null) {
					AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
						smartSingleton.afterSingletonsInstantiated();
						return null;
					}, getAccessControlContext());
				}
				else {
					/**
					 * 如果实现了SmartInitializingSingleton接口,则会调用afterSingletonInstantiated方法
					 *   例如@EventListener注解的实现原理,就是利用EventListenerMethodProcessor后置处理器完成的,
					 *   而在EventListenerMethodProcessor中就是实现了SmartInitializingSingleton接口
					 */
					smartSingleton.afterSingletonsInstantiated();
				}
			}
		}
	}

? ? ? ? 以上的方法大概做了:

  1. 遍历所有的beanNames,触发所有非懒加载单例bean的初始化,即:创建所有的单实例Bean;
  2. 获取beanName对应的MergedBeanDefinition.在实例化之前将所有的beanDefiniton对象在转换成RootBeanDefinition,进行缓存,后续在需要马上实例化的时候直接获取定义信息,而定义信息中,如果对应的beanDefinition包含了父类那就需要实例化父类。
  3. 判断对应的bean不是抽象类 && 是单例 && 不是懒加载才会进行初始化;
  4. 判断是否是factoryBean,然后通过getBean()方法去初始化bean。
	public boolean isFactoryBean(String name) throws NoSuchBeanDefinitionException {
		// 拿到真正的bean的beanName(去掉和&前缀和解析别名之后的)
		String beanName = transformedBeanName(name);
		// 从缓存中获取bean的实例对象.
		Object beanInstance = getSingleton(beanName, false);
		// 如果bean实例不为空,直接判断获取的bean实例是否为FactoryBean
		if (beanInstance != null) {
			return (beanInstance instanceof FactoryBean);
		}
		// No singleton instance found -> check bean definition.
		// 如果缓存中不存在beanName对应的对象 && 父beanFactory为ConfigurableBeanFactory的实例,则调用父BeanFactory判断是否为FactoryBean
		if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory) {
			// No bean definition found in this factory -> delegate to parent.
			return ((ConfigurableBeanFactory) getParentBeanFactory()).isFactoryBean(name);
		}
		// 通过MergedBeanDefinition来判断beanName对应的Bean是否为FactoryBean.
		return isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName));
	}

	protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) {
		// 获取beanName对应的Bean实例的类型
		Class<?> beanType = predictBeanType(beanName, mbd, FactoryBean.class);
		// 返回beanType是否为FactoryBean本身,(子类,或者子接口).
		return (beanType != null && FactoryBean.class.isAssignableFrom(beanType));
	}

?????????如果该bean已经初始化则去缓存中获取bean,并且判断给初始化出来的bean是不是FactoryBean,否则如果当前bean都不在当前bean容器中(存在父子容器),那就调用父容器来判断是否是FactoryBean,否则通过MergedBeanDefinition来判断beanName对应的Bean是否为FactoryBean。

一、FactoryBean和BeanFactory的区别

? ? ? ? 相信面试很多人会被问到这个问题,那他们到底有什么区别呢?

? ? ? ? 首先先说他们的相同点:都是用来创建bean对象的。

? ? ? ? 不同点:如果使用了BeanFactory那么就必须要严格遵循SpringBean的生命周期接口,例如从实例化 ——>初始化等等一系列方法,此流程非常的负责且麻烦,如果使用FactotyBean则创建bean就更加方便简单,不需要遵循spring的生命周期流程,他主要有getObject():直接返回一个对象,isSingleton():判断是否是单例,getObjectTye():返回需要返回对象的类型三个方法。

? ? ? ? 我们来看下一个beanfactory的配置实现:

import org.springframework.beans.factory.FactoryBean;

public class CarFactoryBean implements FactoryBean<Car> {

    private String carInfo;

    public String getCarInfo() {
        return carInfo;
    }

    public void setCarInfo(String carInfo) {
        this.carInfo = carInfo;
    }

    @Override
    public Car getObject() throws Exception {

        Car car = new Car();
        String[] split = carInfo.split(",");
        car.setName(split[0]);
        car.setBrand(split[1]);
        car.setSpeed(Integer.valueOf(split[2]));
        return  car;
    }

    @Override
    public Class<?> getObjectType() {
        return Car.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

? ? ? ? 修改配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<bean id="car" class="org.mqc.factorybean.MyFactoryBean" >
	</bean>
</beans>

? ? ? ? 测试代码:

public class MyTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
        Car car=(Car)context.getBean("car");
        System.out.println(car);
    }
}

?

? ? ? ? 接下来我们来看下如果是一个FactoryBean,Spring是如何进行初始化的。

? ? ? ? 如果是一个FactoryBean,Spring通过beanName获取FactoryBean的实例,factoryBean的名称是:"&" + beanName;

? ? ? ? 我们知道通过调用getBean之后,我们会调doGetBean()然后调用createBean,然后得到一个bean实例(如果对这块不是很熟悉的,可以先去看下之前的Spring源码解析(5)之bean实例化过程(上)_jokeMqc的博客-CSDN博客),然后就会调用getObjectForBeanInstance,我们来看下这个方法的源码:

? ? ? ? ? 我们来看下做了哪些事情:

  1. 调用父类AbstractBeanFactory的getObjectForBeanInstance方法获取bean实例;
  2. 首先会判断名字如果是&开头但是又不是FactoryBean则会提示报错,然后又会判断是否是NullBean;
  3. 如果不是工厂bean或者bean的名称也是不是以&开头,则直接返回上一步中已经创建好的bean对象,所以我们知道getObjectForBeanInstance是用来处理FactoryBean的;
  4. 会去缓存中找,如果找不到则会调用getObjectFromFactoryBean来回调FactoryBean的getObject方法。
	protected Object getObjectForBeanInstance(
			Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {
		// 从ThreadLocal中获取当前正在创建的Bean的名称
		String currentlyCreatedBean = this.currentlyCreatedBean.get();
		if (currentlyCreatedBean != null) {
			registerDependentBean(beanName, currentlyCreatedBean);
		}

		// 调用父类AbstractBeanFactory的getObjectForBeanInstance方法获取bean实例
		return super.getObjectForBeanInstance(beanInstance, name, beanName, mbd);
	}


	protected Object getObjectForBeanInstance(
			Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {

		// Don't let calling code try to dereference the factory if the bean isn't a factory.
		// 不为null而且以&开头
		if (BeanFactoryUtils.isFactoryDereference(name)) {
			if (beanInstance instanceof NullBean) {
				return beanInstance;
			}
			if (!(beanInstance instanceof FactoryBean)) {
				throw new BeanIsNotAFactoryException(beanName, beanInstance.getClass());
			}
		}

		// Now we have the bean instance, which may be a normal bean or a FactoryBean.
		// If it's a FactoryBean, we use it to create a bean instance, unless the
		// caller actually wants a reference to the factory.
		// 如果不是工厂bean或者bean的名称也是不是以&开头,则直接返回上一步中已经创建好的bean对象.
		if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
			return beanInstance;
		}

		Object object = null;
		if (mbd == null) {
			// 先从FactoryBean对应的缓存factoryBeanObjectCache中获取
			object = getCachedObjectForFactoryBean(beanName);
		}

		if (object == null) {
			// Return bean instance from factory.
			FactoryBean<?> factory = (FactoryBean<?>) beanInstance;

			// Caches object obtained from FactoryBean if it is a singleton.
			if (mbd == null && containsBeanDefinition(beanName)) {
				mbd = getMergedLocalBeanDefinition(beanName);
			}
			// bean定义不为空而且对应的class类为一个合成类.
			boolean synthetic = (mbd != null && mbd.isSynthetic());

			// 回调FactoryBean的getObject方法
			object = getObjectFromFactoryBean(factory, beanName, !synthetic);
		}
		return object;
	}

? ? ? ? 我们来看下getObjectFromFactoryBean的源码:

  1. 首先会判断该bean是不是单例的,如果是多例的,每次都会调用子类的getObject方法进行bean实例的创建;
  2. 根据名称从factoryBeanObjectCache缓存中获取由FactoryBean创建的bean实例,如果未获取到,则表示之前还没有创建过,则调用FactoryBean子类的getObject方法进行创建,后将创建成功的bean实例放入到factoryBeanObjectCache缓存中;
  3. 调用doGetObjectFromFactoryBean回调FactoryBean的getObject方法;
  4. 放入factoryBeanObjectCache缓存中;
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
		// 如果是单例的
		if (factory.isSingleton() && containsSingleton(beanName)) {
			synchronized (getSingletonMutex()) {
				/**
				 * 首先根据名称从factoryBeanObjectCache缓存中获取由FactoryBean创建的bean实例。
				 *  如果未获取到,则表示之前还没有创建过,则调用FactoryBean子类的getObject方法进行创建,
				 *  然后将创建成功的bean实例放入到factoryBeanObjectCache缓存中
				 */
				Object object = this.factoryBeanObjectCache.get(beanName);
				if (object == null) {

					// 回调FactoryBean的getObject方法
					object = doGetObjectFromFactoryBean(factory, beanName);

					// Only post-process and store if not put there already during getObject() call above
					// (e.g. because of circular reference processing triggered by custom getBean calls)
					Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
					if (alreadyThere != null) {
						object = alreadyThere;
					}
					else {
						if (shouldPostProcess) {
							if (isSingletonCurrentlyInCreation(beanName)) {
								// Temporarily return non-post-processed object, not storing it yet..
								return object;
							}
							// bean创建之后进行一些校验操作
							beforeSingletonCreation(beanName);
							try {
								// 使用factoryBean创建bean实例之前的后置处理
								object = postProcessObjectFromFactoryBean(object, beanName);
							}
							catch (Throwable ex) {
								throw new BeanCreationException(beanName,
										"Post-processing of FactoryBean's singleton object failed", ex);
							}
							finally {
								afterSingletonCreation(beanName);
							}
						}
						// 将由FactoryBean创建的Bean实例放入到factoryBeanObjectCache缓存中
						if (containsSingleton(beanName)) {
							this.factoryBeanObjectCache.put(beanName, object);
						}
					}
				}
				return object;
			}
		}
		else {
			// 如果是多例的,每次都会调用子类的getObject方法进行bean实例的创建
			Object object = doGetObjectFromFactoryBean(factory, beanName);
			if (shouldPostProcess) {
				try {
					object = postProcessObjectFromFactoryBean(object, beanName);
				}
				catch (Throwable ex) {
					throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);
				}
			}
			return object;
		}
	}

????????

	private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName)
			throws BeanCreationException {

		Object object;
		try {
			if (System.getSecurityManager() != null) {
				AccessControlContext acc = getAccessControlContext();
				try {
					object = AccessController.doPrivileged((PrivilegedExceptionAction<Object>) factory::getObject, acc);
				}
				catch (PrivilegedActionException pae) {
					throw pae.getException();
				}
			}
			else {
				// 调用FactoryBean子类的getObject方法去获取bean实例.
				object = factory.getObject();
			}
		}
		catch (FactoryBeanNotInitializedException ex) {
			throw new BeanCurrentlyInCreationException(beanName, ex.toString());
		}
		catch (Throwable ex) {
			throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex);
		}

		// Do not accept a null value for a FactoryBean that's not fully
		// initialized yet: Many FactoryBeans just return null then.
		if (object == null) {
			if (isSingletonCurrentlyInCreation(beanName)) {
				throw new BeanCurrentlyInCreationException(
						beanName, "FactoryBean which is currently in creation returned null from getObject");
			}

			// 未获取到bean时,返回一个空Bean。NullBean对象
			object = new NullBean();
		}
		return object;
	}

?????????可以看得到我们是回到getObject方法来创建实例,然后把创建好的实例对象放入到factoryBeanObjectCache缓存中。

?????????

? ? ? ? ?可以看得到我们的FactoryBean的实例是放在一级缓存中的,而我们通过BeanFactory的getObject()方法获取得到的bean是放在factoryBeanObjectCache缓存中的。

? ? ? ? 好了到这里factoryBean的源码已经分析完毕了,这里提出一个疑问?在Spring中如果我们要在一个单例bean中保存一个protype类型的bean,那该怎么操作?

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 15:57:54  更:2022-03-03 16:01:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 12:07:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码