简介: 项目的初衷是独立作出一个成熟的有特色的IOC容器,但由于过程参考Spring太多,而且也无法作出太多改进,于是目的变为以此项目作为理解Spring的一个跳板,与网上的一些模仿Spring的框架不同,本项目主要是针对注解形式

概述

项目的初衷是独立作出一个成熟的有特色的IOC容器,但由于过程参考Spring太多,而且也无法作出太多改进,于是目的变为以此项目作为理解Spring的一个跳板,与网上的一些模仿Spring的框架不同,本项目主要是针对注解形式
地址是Thales

流程

在Spring中,一个bean的形成分三个大的阶段,

  1. bean的定义阶段(包含BeanDefinition的加载,解析,与注册)
  2. bean的实例化阶段(包含对象的创建,属性的注入)
  3. bean的初始化阶段(包含一些资源的初始化,譬如打开文件建立连接等等)

这只是大概的划分,关于BeanPostProcessor等后置处理并没有显式的提及.

类的设计

如果只想了解一个bean是怎么从生到死的,只需要一步步debug就好了,如果看不懂,就多debug几遍.可是如果想实现一个类似的容器,类的设计,职责的分配,接口的实现继承必然是要了解的(除非你想几个类做完所有的事)

以下是DefaultListableBeanFactory的类图

是不是顶不住

我们再来看一张图

第一张是Spring5.0的,第二张图是Spring0.9的,所以并没有必要在一开始就引入过多的设计复杂度

我们再来看一套对比图

哪一个是0.9的,哪一个是5.0的一目了然.

说这么多的目的,是说明我们没必要一开始就奔着最完善的目标去写,可以一步步来,一步步加入功能

实现简易IOC

众所周知,SpringIoC中最基本的就是BeanFactory

我们先定义一个BeanFactory接口

//暂时就给这一个方法
public interface BeanFactory {/*** 根据名字获取Bean实例* @param name* @return*/Object getBean(String name);
}

beanDefinition

由于是注解形式,我们不能再像xml那样给定一个资源文件再去解析了,而应该去扫描classPath下所有带有@Component的类,

这时候我们需要给定的参数就从文件路径变成了包路径,我们只需要扫描这个包及其子包内符合条件的类,并且将其转化为BeanDefinition再注册就好.执行这个功能的是ClassPathBeanDefinitionScanner这个类.在这一步,就已经和传统的流程有所区别了,我们会传入一个ResourceLoader去实现具体的扫描功能(即定位),但不会再有专门的类去处理解析这一步

public interface Resource {File getFile();String getFilename();String getFilePath();
}
//在最初设计的时候这个抽象类似乎没有用,但考虑到以后的扩展,还是先放在这
public abstract class AbstractResource implements Resource {@Overridepublic String getFilename() {return getFile().getName();}@Overridepublic String getFilePath() {return getFile().getPath();}
}
//这就是最终我们实例化bean时用到的Resource类,在Spring中并没有直接用,而是通过外观模式集成了一下成为RootBeanDefinition
public class ClassPathResource extends AbstractResource {private final String path;private ClassLoader classLoader;private Class<?> clazz;public ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) {this.path = path;this.classLoader = classLoader;this.clazz = clazz;}
}
public interface ResourceLoader {Resource getResource(String location);
}
//此类能够实现加载多个资源
public interface ResourcePatternResolver extends ResourceLoader {String CLASSPATH_ALL_URL_PREFIX = "classpath*:";List<? extends Resource> getResources(String location);
}//这个类就是正式用于扫描的类了
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {private final ResourceLoader resourceLoader;public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader){this.resourceLoader = resourceLoader;}@Overridepublic Resource getResource(String location) {return resourceLoader.getResource(location);}
//在Spring中,是通过一层层方法的包装完成包名到路径的转换再到每个文件的扫描再转换为Resource,这里暂时就先一步到位,把具体实现放在工具类里@Overridepublic List<? extends Resource> getResources(String location) {Set<Class<?>> classes = ClassUtils.getClasses(location);List<ClassPathResource> classPathResources = new ArrayList<>();for (Class<?> clazz:classes) {classPathResources.add(new ClassPathResource("",clazz.getClassLoader(),clazz));}return classPathResources;}
}

但最后直接使用的并不是PathMatchingResourcePatternResolver

而是把他作为ClassPathBeanDefinitionScanner的一个属性,在这个类里调用.

我们得到了Resource,如何获得对应的BeanDefinition?

先考虑这样一个问题,什么样的类可以被注册BeanDefinition?

  1. 添加了@Component注解或者满足其他注册的条件
  2. 不是接口或者抽象类

所以我们可以单独抽象出一个方法 boolean isCandidateComponent(Class<?> clazz)来判断是否被注册

现在到了注册阶段,依旧秉持面向接口编程的理念,同时考虑到单一职责,我们把注册Bean定义单独抽象出来

public interface BeanDefinitionRegistry {void registerBeanDefinition(BeanDefinition beanDefinition);
}

上文说到Bean定义的定位,解析,注册都是在ClassPathBeanDefinitionScanner里完成的,于是BeanDefinitionRegistry自然也成为了ClassPathBeanDefinitionScanner的属性之一

于是ClassPathBeanDefinitionScanner构建完成了

public class ClassPathBeanDefinitionScanner {//负责具体的Resource定位private ResourcePatternResolver resourcePatternResolver;//负责BeanDefinition解析private BeanDefinitionRegistry registry;public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry,String...basePackage) {this.registry = registry;this.resourcePatternResolver = new PathMatchingResourcePatternResolver((ResourceLoader) registry);this.scan(basePackage);}public void scan(String...basePackages){doScan(basePackages);}void doScan(String[] basePackages){Set<BeanDefinition> beanDefinitions = new LinkedHashSet<>();for (String basePackage:basePackages) {Set<BeanDefinition> candidates = findCandidateComponents(basePackage);for(BeanDefinition candidate:candidates){beanDefinitions.add(candidate);registry.registerBeanDefinition(candidate);}}}//获取被注册的bean的集合private Set<BeanDefinition> findCandidateComponents(String basePackage) {Set<BeanDefinition> candidates = new LinkedHashSet<>();List<? extends Resource> resources = getResourcePatternResolver().getResources(basePackage);for(Resource resource:resources){if(resource instanceof ClassPathResource){ClassPathResource classPathResource = (ClassPathResource)resource;if(isCandidateComponent(classPathResource.getClazz())){AnnotationBeanDefinition beanDefinition = new AnnotationBeanDefinition();beanDefinition.setClazz(classPathResource.getClazz());beanDefinition.setBeanName(BeanUtils.generateBeanName(classPathResource.getClazz().getName()));candidates.add(beanDefinition);}}}return candidates;}private ResourcePatternResolver getResourcePatternResolver() {return this.resourcePatternResolver;}//判断是否被注册boolean isCandidateComponent(Class<?> clazz){Component declaredAnnotation = clazz.getDeclaredAnnotation(Component.class);return declaredAnnotation!=null&&!clazz.isInterface();};
}

实例化

在什么时候实例化?我们说,在调用getBean()而又没有现成的bean时进行实例化

public abstract class AbstractBeanFactory implements BeanFactory{@Overridepublic Object getBean(String beanName)}

对象创建

有两种方式,通过Jdk默认的反射实现,或者用cglib代理实现.

默认自然是无参构造,但是如果传入了参数,则需要根据参数的类型和数量去匹配对应的构造函数,用其去实例化

于是我们抽象出InstantiationStrategy作为实例化接口,两种实例化方法都需要实现这个接口,我们真正去用的时候只需要去调该接口的方法就好

public interface InstantiationStrategy {Object instantiate(BeanDefinition beanDefinition, String beanName, BeanFactory owner);
}
public class SimpleInstantiationStrategy implements InstantiationStrategy {
}

属性注入

字段值获取

有两种方式可以实现字段值获取

  1. 直接注解Autowired或者Value
  2. Value里面填的不是值而是占位符,那么就需要解析占位符去获取

我们通过Class对象获取所有字段,再通过遍历所有字段查找加在字段上的注解来获取(这仅仅只是Spring的一种注入方式)

 //处理@Autowired注解for(Field field:declaredFields){Autowired autowired = field.getDeclaredAnnotation(Autowired.class);if(autowired != null){pvs.add(new PropertyValue(field.getName(),new BeanReference(BeanUtils.generateBeanName(field.getType().getName()),field.getType())));}}//处理@Value注解for(Field field:declaredFields){Value value = field.getDeclaredAnnotation(Value.class);if(value != null){String value1 = value.value();pvs.add(new PropertyValue(field.getName(),value1));}}

字段值填充

获取字段值后通过反射填入相应的字段中

for(Field field:mbd.getBeanClass().getDeclaredFields()){field.setAccessible(true);if (field.getName().equals(propertiesValue.getName())&&field.getType().isAssignableFrom(newValue.getClass())) {field.set(bean,newValue);}}

初始化

调用指定的初始化方法,进行资源的初始化.,如何获取初始化方法?在xml模式中,只要加个标签即可,如果是注解模式,加个注解标识一下或者在某个注解上加个参数,代表初始化方法,这个还没有实现

功能填充

后置处理器添加

上面我们已经实现了一个可以进行依赖查找,依赖注入的Bean容器,让我们再回顾一下Spring的流程,我们少了些什么,最容易想到的应该就是后置处理器了,包括BeanFactoryPostProcessorBeanPostProcessor两种,前者对于beanFactory进行修改操作,后者对于bean进行修改操作,同样是面向接口编程

首先建立BeanPostProcessor

public interface BeanPostProcessor {Object postProcessBeforeInitialization(Object bean, String beanName);Object postProcessAfterInitialization(Object bean, String beanName) ;
}

就目前来看,有什么是需要BeanPostProcessor来做的呢?我们可以把之前对注解进行处理,获取注入属性的代码分离出来,专门用一个BeanPostProcessor去处理

所有自定义实现的BeanPostProcessor都需要继承这个接口,由于BeanPostProcessor的作用是处理其他的Bean,所以必须要在其他被处理的Bean实例化之前被创建出来.于是我们在finishBeanFactoryInitialization(beanFactory);之前添加registerBeanPostProcessors(beanFactory);用于实例化所有的BeanPostProcessor

而这些beanPostProcessor的重要程度是不同的,例如处理注解注入的BeanPostProcessor优先级就要比一般的BeanPostProcessor优先级要高,所以需要先实例化

Aware接口添加

其实现在我们已经可以完全的把一个对象交由IOC容器了,但此时这个对象与容器之间的关系是单向的,容器能够操作bean,但bean不能借助容器,为了解决此类问题,我们添加一个Aware接口作为标志接口,由各个更具体的Aware去继承他,并在实例化属性之后,初始化方法执行之完成相关容器属性的注入

事件监听器添加

监听器是观察者模式的一种实现

我们先定义以下几个基本接口

public interface ApplicationEventPublisher {/*** 发布事件* @param event*/void publishEvent(ApplicationEvent event);
}public interface ApplicationEventMulticaster {/*** 添加广播事件* @param event*/void multicastEvent(ApplicationEvent event);/*** 添加对于某个事件的监听器* @param listener*/void addApplicationListener(ApplicationListener listener);/*** 移除指定监听器* @param listener*/void removeApplicationListener(ApplicationListener listener);
}
public interface ApplicationListener <E extends ApplicationEvent> extends EventListener {/*** 监听特定事件* @param event*/void onApplicationEvent(E event);
}

具体调用流程为具体的listener被添加到广播器中,事件通过publisher统一发布,而publishEvent最后会调用 multicastEvent(ApplicationEvent event)方法,经过相应判断后由对应监听器做出相应操作.

如何判断这个监听器是否对该事件感兴趣?

我们事先实现的listener是有泛型的,我们可以通过这个泛型与传入的事件类型的关系来判断

public boolean supportEvent(ApplicationListener<ApplicationEvent> listener,ApplicationEvent event){//先获取Class对象Class<? extends ApplicationListener> listenerClass = listener.getClass();//获取其实现的所有接口(包括泛型信息)Type[] genericInterfaces = listenerClass.getGenericInterfaces();for (Type genericInterface:genericInterfaces){//判断是否为泛型接口if(genericInterface instanceof ParameterizedType){ParameterizedType parameterizedType = (ParameterizedType) genericInterface;//得到所有泛型参数Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();for(Type actualTypeArgument:actualTypeArguments){try {Class<?> aClass = Class.forName(actualTypeArgument.getTypeName());//判断感兴趣的事件类型是否与传入事件相同,或者是其父类if(aClass.isAssignableFrom(event.getClass())){return true;}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}return false;}

FactoryBean添加

目前的Bean都是由BeanFactory来产生的,

我们用FactoryBean接口来标识这个产生Bean的特殊的Bean

循环依赖的解决

循环依赖是指A依赖于B的同时B依赖于A,解决方法为实例化与初始化分离,如果只考虑一般情况的话用两级缓存实际上就够了,

代码优化

实现简易AOP

如果从正统的AOP开始的话,随之而来的就是一堆概念,包括切点,通知一类

我们先看AOP要做什么

所以说AOP的核心就是动态代理,我们以Cglib为例来看看动态代理要怎么用

 Enhancer enhancer = new Enhancer();
//1. 为哪个类进行代理enhancer.setSuperclass(Buy.class);enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {//2. 为该类的哪个方法进行代理if(method.getName().equals("buyOne")){//3. 代理究竟要做什么System.out.println("hello");}//4. 调用原有的对象methodProxy.invokeSuper(o,objects);return o;});
//5. 产生代理后的对象Buy o = (Buy)enhancer.create();

这就是动态代理最核心的功能,也是AOP的核心功能,AOP的最终目的是代码5,即产生一个代理对象,把这个代理对象交给IOC去管理

而为了达成这个目的,AOP框架需要做好代码1-4所需要做的事,一和二组合起来,成了JoinPoint,3叫做Advice,这两个组合起来就叫做Advisor,可不可以不分这些种类,就全写在一个或几个类里,当然可以,Spring0.9就是这么做的,但发展到如今,早已采用了这种划分方式.本项目也采用这种分类.

先从连接点说起,如何确定到底在哪里实现功能增强,无非是类与方法两个层次;

我们先定义ClassFilterMethodMacther两个接口

public interface ClassFilter {/*** 给定类型是否匹配* @param clazz* @return*/boolean matches(Class< ? > clazz);
}
public interface MethodMatcher {/*** 对应类的对应方法是否匹配* @param method* @param targetClass* @return*/boolean matches(Method method,Class< ? > targetClass);
}

这两个接口必然是组合起来使用的,于是我们用PointCut将其组合起来

public interface Pointcut {/*** 获取ClassFilter* @return*/ClassFilter getClassFilter();/*** 获取MethodMatcher* @return*/MethodMatcher getMethodMatcher();
}

接口只是定义了抽象功能,这些功能还要有具体的实现

我们默认用Java的正则去匹配方法名,以此构建出JdkRegexMethodMatcher

public class JdkRegexMethodPointcut implements MethodMatcher, Pointcut{private Pattern[] compiledPatterns = new Pattern[0];@Overridepublic ClassFilter getClassFilter() {return null;}@Overridepublic MethodMatcher getMethodMatcher() {return this;}@Overridepublic boolean matches(Method method, Class<?> targetClass) {String name = method.getName();for (Pattern pattern :compiledPatterns) {Matcher matcher = pattern.matcher(name);if(matcher.matches()){return true;}}return false;}//预编译private Pattern[] compilePatterns(String[] source) throws PatternSyntaxException {Pattern[] destination = new Pattern[source.length];for (int i = 0; i < source.length; i++) {destination[i] = Pattern.compile(source[i]);}return destination;}public void initPatternRepresentation(String[] patterns) throws PatternSyntaxException {this.compiledPatterns = compilePatterns(patterns);}
}

Spring中,并不是直接继承的MethodMatcher,考虑到正则的语法不同,额外做了一层抽象,但在此处省略掉了

JdkRegexMethodMatcher同时也实现了PointCut类,也就是说,现在切点已经准备好了

再来看Advice

由于考虑的可扩展点比较多,于是继承的层次也变的多了

public interface Advice {
}
public interface BeforeAdvice extends Advice{
}
public interface MethodBeforeAdvice extends BeforeAdvice{void before(Method method, Object[] args, Object target) throws Throwable;
}

现在Advice也定义完了,具体的实现我们交由用户去做

接下来就是整合成Advisor

public interface Advisor {Advice getAdvice();
}
public interface PointcutAdvisor extends Advisor{Pointcut getPointcut();
}
public abstract class AbstractPointcutAdvisor implements PointcutAdvisor{private Advice advice;@Overridepublic Advice getAdvice() {return advice;}public void setAdvice(Advice advice) {this.advice = advice;}
}

目前已经定义好了Advisor的功能

我们再实现这个接口

public class RegexMethodPointcutAdvisor extends AbstractPointcutAdvisor {JdkRegexMethodPointcut pointcut = new JdkRegexMethodPointcut();private String[] patterns;public RegexMethodPointcutAdvisor() {}public RegexMethodPointcutAdvisor(Advice advice) {setAdvice(advice);}public void setPattern(String pattern) {setPatterns(pattern);}public void setPatterns(String... patterns) {this.patterns = patterns;pointcut.initPatternRepresentation(patterns);}@Overridepublic Pointcut getPointcut() {return pointcut;}
}

RegexMethodPointcutAdvisor就整合了PointCut以及Advice,通过他,我们就可以确定在何处做何种增强.

现在的advisor可以完成检验一个类是否要被代理的功能,但是如果这个类需要被代理,advisor却无法保存这个类的对应信息

于是我们需要一个类将advisor与对应的代理类结合起来,这就是AdvisedSupport

public class AdvisedSupport {private  TargetSource targetSource;private List<MethodInterceptor> methodInterceptors = new ArrayList<>();private List<PointcutAdvisor> advisors = new ArrayList<>();public TargetSource getTargetSource() {return targetSource;}public void setTargetSource(TargetSource targetSource) {this.targetSource = targetSource;}public List<MethodInterceptor> getMethodInterceptor() {return methodInterceptors;}public void addMethodInterceptor(MethodInterceptor methodInterceptor) {this.methodInterceptors.add(methodInterceptor);}public List<PointcutAdvisor> getAdvisor() {return advisors;}public void addAdvisor(PointcutAdvisor advisor) {MethodBeforeAdviceInterceptor methodBeforeAdviceInterceptor = new MethodBeforeAdviceInterceptor();methodBeforeAdviceInterceptor.setAdvice((MethodBeforeAdvice) advisor.getAdvice());addMethodInterceptor(methodBeforeAdviceInterceptor);this.advisors.add(advisor);}
}

上类属性中的TargetSource便是真正持有代理对象信息的类

现在万事具备,只需要用Cglib去使用我们已经持有的信息就可以创建出新的类了

public class CglibAopProxy implements AopProxy{private final AdvisedSupport advised;public CglibAopProxy(AdvisedSupport advised) {this.advised = advised;}@Overridepublic Object getProxy() {Enhancer enhancer = new Enhancer();//1. 为哪个类进行代理enhancer.setSuperclass(advised.getTargetSource().getTargetClass());enhancer.setCallback(new DynamicAdvisedInterceptor(advised));//5. 产生代理后的对象return enhancer.create();}private static class DynamicAdvisedInterceptor implements MethodInterceptor {private final AdvisedSupport advised;public DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {CglibInvocation cglibInvocation = new CglibInvocation(method,objects,o,methodProxy);//2. 为该类的哪个方法进行代理for(PointcutAdvisor advisor: advised.getAdvisor()){if(advisor.getPointcut().getMethodMatcher().matches(method,advised.getTargetSource().getTargetClass())){//3. 代理究竟要做什么return advised.getMethodInterceptor().get(0).invoke(cglibInvocation);}}//4. 调用源方法return cglibInvocation.proceed();}}
}

将这份代码与最初使用cglib的代码比较,会发现过程几乎是一模一样.但是作为一个框架,应该尽可能的给用户以方便

于是我们需要一个Creator去把这一切都做好,他需要负责将AdvicePointCut组合成Advisor,再将AdvisorTargetSource组装成AdvisedSupport,再将AdvisedSupport交给Cglib动态代理,产生代理对象,而用户只需要编写Advice以及切入点表达式即可

功能演示

  1. 属性注入

    1. 基本类型
    2. 引用类型
    3. 循环依赖
  2. 容器感知
  3. FactoryBean生成对象
  4. AOP切面增强
  5. 自定义BeanPostProcessor

困难及解决

  1. 首先是设计上的问题
  2. FactoryBean的实现
  3. AOP与IOC的结合
  4. 字段的注入

原文链接

本文为阿里云原创内容,未经允许不得转载。

模仿Spring实现一个类管理容器相关推荐

  1. Qt封装一个类管理moveToThread( )正确的开启多线程、安全的退出线程的实例

    看本篇的文章基础在于你已经懂得了简单的利用moveToThread的方法创建出一个线程. 不会的话可以查看本篇博客 总结:QT 多线程(处理密集时的界面响应保持) 运用: MultMoveToThre ...

  2. spring 配置 一个类在 tomcat启动的时候调用

    配置一个扫描 init包下面所有的类 打上注解,实现接口,注入service.

  3. Spring 类管理机制

    前言 Spring作为JAVA中最流行的框架,当年横空出世的时候,是把JAVA从死亡边缘拉了回来.之前的JAVA类都需要自行管理的,导致在进行J2EE大型开发的时候完全力不从心,而且因为JAVA的继承 ...

  4. 从一个例子开始体验轻量级类隔离容器 SOFAArk | SOFAChannel#11 直播整理

    <SOFA:Channnel/>,有趣实用的分布式架构频道. 本文根据 SOFAChannel#11 直播分享整理,主题:从一个例子开始体验轻量级类隔离容器 SOFAArk.回顾视频以及 ...

  5. 【Spring】普通类获取Spring容器的bean的方法

    我们通常在项目中获取Spring容器里bean的方式,一般是使用注解的方式(@Autowired.@Resource)直接注入就可以直接使用了,那么如果在一个普通的类里(其他地方使用它的实例是以new ...

  6. Spring框架学习笔记04:初探Spring——采用Java配置类管理Bean

    文章目录 一.课程引入 二.采用Java配置类管理Bean (一)打开项目[SpringDemo2021] (二)创建net.hw.spring.lesson04包 (三)创建杀龙任务类 (四)创建勇 ...

  7. C++:利用静态成员的方法实现对班费的管理。要求定义一个类Student,除了声明一个存放班费的静态成员,还要求分别定义一个上交班费的成员函数Contribute()、花费班费的成员函数Spend(

    C++:利用静态成员的方法实现对班费的管理.要求定义一个类Student,除了声明一个存放班费的静态成员,还要求分别定义一个上交班费的成员函数Contribute().花费班费的成员函数Spend() ...

  8. 创建一个Worker类,具有属性 name sex salary level 创建一个linkedlist作为容器.录入5条工人信息

    创建一个Worker类,具有属性 name sex salary level 创建一个linkedlist作为容器,数据源定义在main方法中 写出一下方法: 1.录入5条工人信息 2.输出所有工人信 ...

  9. 证明spring中property name=这个双引号的内容只与setter方法有关,与一个类定义的字段和getter方法无关...

    证明如下: 思路定义两个实体类每个实体类的成员变量(字段)名和setter 和getter的名字都不一样: 原因是:bean的声明周期的原因:有一步是:注入属性. 其中一个类引用了另一个类. 被引用类 ...

最新文章

  1. curl 命令行下载工具使用方法小结
  2. Pytorch多进程最佳实践
  3. 算法基础数学知识篇(1)之----- 排列数组
  4. 最全总结,GitHub Action自动化部署
  5. ubuntu java对比win_Ubuntu PK Vista Java性能大比拼
  6. 抖音矩阵号系统搭建及开发思路分享丨抖音短视频关键词优化
  7. 对接有道翻译api中英翻译软件
  8. stm32智能小车设计
  9. Python网络爬虫之爬取微博热搜
  10. 五年级计算机教学要点,五年级信息技术教学工作计划
  11. ps -aux排序--按内存使用排序或按cpu使用排序
  12. 修改gh-ost源码实现两表在线高速复制
  13. 如何从TI官网下载芯片的AltiumDesigner原理图文件和封装文件
  14. 人工智能实践作业-修道士和野人过河问题
  15. 如何阅读Smalltalk程序
  16. 苹果汽车已上路测试,预计将于明年推出
  17. 程序员老了怎么办?做什么好?
  18. 使用NFC tool工具将加密门禁卡写入小米手环步骤详解
  19. 禁止宣传高考状元,学校秒变果园。。
  20. 可视化学习笔记4:使用颜色

热门文章

  1. JAVA入门级教学之(方法-题-3)
  2. php表单提交邮箱_最全实现dede订单表单提交发送到指定邮箱(附前台设置)
  3. svn复制出来的java_从svn下载的项目(或从别处拷贝来的)报错的可能情况以及解决经验...
  4. 所有的图放到一个html,拖放是HTML5标准的组成部分,若想要把drag1图片放入d
  5. java中容易被忽视的基本概念
  6. tomcat.exe java home,tomcat.exe启动和startup.bat启动的不同
  7. python 获取文件列表_Python3 - 获取文件夹中的文件列表
  8. redis重启命令_请收下这份redis持久化详解
  9. scripts文件夹_常用Scripts整理
  10. 单片机传输浮点数给android,请问单片机怎么接收从串口发送过来的浮点数?