前言

我们的框架,到了今天,其实已经比较完备了。Spring的两大特性:IOC和AOP,都已经被我们实现了。这让我想到了那句名言:【物理学的大厦已经落成,上面只有两朵乌云】(flag就这么立起来了)。那对于我们的框架来说,乌云是什么呢?易用性。我们虽然实现了这些基本功能,但是用起来总觉得很古老,和我们现在熟悉的Spring有所区别。我们现在根本不会去配置文件里面一个一个的配置bean,而是直接用注解来完成。只能说懒惰是第一生产力,人类为了偷懒,真的是煞费苦心。已经提供了配置文件这种方式,但还是不满足,还是要更进一步。那么这次,我们就来实现注解的自动化Bean注册。除此之外,我们还要实现一个配置文件的占位符替换。什么意思呢?我们经常会出现在xml配置中使用${xxx}的例子,这里的值,则是放在yml文件中。我们这里就是要实现这种将占位符替换为真正的值这样一个过程。

工程结构

├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─akitsuki
│  │  │          └─springframework
│  │  │              ├─aop
│  │  │              │  │  AdvisedSupport.java
│  │  │              │  │  Advisor.java
│  │  │              │  │  BeforeAdvice.java
│  │  │              │  │  ClassFilter.java
│  │  │              │  │  MethodBeforeAdvice.java
│  │  │              │  │  MethodMatcher.java
│  │  │              │  │  Pointcut.java
│  │  │              │  │  PointcutAdvisor.java
│  │  │              │  │  TargetSource.java
│  │  │              │  │
│  │  │              │  ├─aspect
│  │  │              │  │      AspectJExpressionPointcut.java
│  │  │              │  │      AspectJExpressionPointcutAdvisor.java
│  │  │              │  │
│  │  │              │  └─framework
│  │  │              │      │  AopProxy.java
│  │  │              │      │  Cglib2AopProxy.java
│  │  │              │      │  JdkDynamicAopProxy.java
│  │  │              │      │  ProxyFactory.java
│  │  │              │      │  ReflectiveMethodInvocation.java
│  │  │              │      │
│  │  │              │      ├─adapter
│  │  │              │      │      MethodBeforeAdviceInterceptor.java
│  │  │              │      │
│  │  │              │      └─autoproxy
│  │  │              │              DefaultAdvisorAutoProxyCreator.java
│  │  │              │
│  │  │              ├─beans
│  │  │              │  ├─exception
│  │  │              │  │      BeanException.java
│  │  │              │  │
│  │  │              │  └─factory
│  │  │              │      │  Aware.java
│  │  │              │      │  BeanClassLoaderAware.java
│  │  │              │      │  BeanFactory.java
│  │  │              │      │  BeanFactoryAware.java
│  │  │              │      │  BeanNameAware.java
│  │  │              │      │  ConfigurableListableBeanFactory.java
│  │  │              │      │  DisposableBean.java
│  │  │              │      │  FactoryBean.java
│  │  │              │      │  HierarchicalBeanFactory.java
│  │  │              │      │  InitializingBean.java
│  │  │              │      │  ListableBeanFactory.java
│  │  │              │      │  PropertyPlaceholderConfigurer.java
│  │  │              │      │
│  │  │              │      ├─config
│  │  │              │      │      AutowireCapableBeanFactory.java
│  │  │              │      │      BeanDefinition.java
│  │  │              │      │      BeanDefinitionRegistryPostProcessor.java
│  │  │              │      │      BeanFactoryPostProcessor.java
│  │  │              │      │      BeanPostProcessor.java
│  │  │              │      │      BeanReference.java
│  │  │              │      │      ConfigurableBeanFactory.java
│  │  │              │      │      DefaultSingletonBeanRegistry.java
│  │  │              │      │      InstantiationAwareBeanPostProcessor.java
│  │  │              │      │      PropertyValue.java
│  │  │              │      │      PropertyValues.java
│  │  │              │      │      SingletonBeanRegistry.java
│  │  │              │      │
│  │  │              │      ├─support
│  │  │              │      │      AbstractAutowireCapableBeanFactory.java
│  │  │              │      │      AbstractBeanDefinitionReader.java
│  │  │              │      │      AbstractBeanFactory.java
│  │  │              │      │      BeanDefinitionReader.java
│  │  │              │      │      BeanDefinitionRegistry.java
│  │  │              │      │      CglibSubclassingInstantiationStrategy.java
│  │  │              │      │      DefaultListableBeanFactory.java
│  │  │              │      │      DisposableBeanAdapter.java
│  │  │              │      │      FactoryBeanRegistrySupport.java
│  │  │              │      │      InstantiationStrategy.java
│  │  │              │      │      SimpleInstantiationStrategy.java
│  │  │              │      │
│  │  │              │      └─xml
│  │  │              │              XmlBeanDefinitionReader.java
│  │  │              │
│  │  │              ├─context
│  │  │              │  │  ApplicationContext.java
│  │  │              │  │  ApplicationContextAware.java
│  │  │              │  │  ApplicationEvent.java
│  │  │              │  │  ApplicationEventPublisher.java
│  │  │              │  │  ApplicationListener.java
│  │  │              │  │  ConfigurableApplicationContext.java
│  │  │              │  │
│  │  │              │  ├─annotation
│  │  │              │  │      ClassPathBeanDefinitionScanner.java
│  │  │              │  │      ClassPathScanningCandidateComponentProvider.java
│  │  │              │  │      Scope.java
│  │  │              │  │
│  │  │              │  ├─event
│  │  │              │  │      AbstractApplicationEventMulticaster.java
│  │  │              │  │      ApplicationContextEvent.java
│  │  │              │  │      ApplicationEventMulticaster.java
│  │  │              │  │      ContextClosedEvent.java
│  │  │              │  │      ContextRefreshEvent.java
│  │  │              │  │      SimpleApplicationEventMulticaster.java
│  │  │              │  │
│  │  │              │  └─support
│  │  │              │          AbstractApplicationContext.java
│  │  │              │          AbstractRefreshableApplicationContext.java
│  │  │              │          AbstractXmlApplicationContext.java
│  │  │              │          ApplicationContextAwareProcessor.java
│  │  │              │          ClasspathXmlApplicationContext.java
│  │  │              │
│  │  │              ├─core
│  │  │              │  └─io
│  │  │              │          ClasspathResource.java
│  │  │              │          DefaultResourceLoader.java
│  │  │              │          FileSystemResource.java
│  │  │              │          Resource.java
│  │  │              │          ResourceLoader.java
│  │  │              │          UrlResource.java
│  │  │              │
│  │  │              ├─stereotype
│  │  │              │      Component.java
│  │  │              │
│  │  │              └─util
│  │  │                      ClassUtils.java
│  │  │
│  │  └─resources
│  └─test
│      ├─java
│      │  └─com
│      │      └─akitsuki
│      │          └─springframework
│      │              └─test
│      │                  │  ApiTest.java
│      │                  │
│      │                  └─bean
│      │                          UserDao.java
│      │                          UserService.java
│      │
│      └─resources
│              application.yml
│              spring.xml

占位符,先拿你开刀

我们的目的很明确,要替换掉配置中的占位符,并且将真正的值替换进去。那么我们首先要思考,这一步应该放在哪里。回顾一下Bean的生命周期,答案应该是Bean定义加载完成,准备实例化Bean的时候。因为我们的占位符,实际上是用来表示Bean中的属性的,也就是写在Bean定义中。那么我们就要在这个时候,修改Bean定义,将其替换成真正的值,然后再水到渠成的执行Bean的创建过程。我们之前刚好有一个后置处理器可以完成这件事情:BeanFactoryPostProcessor

package com.akitsuki.springframework.beans.factory;import com.akitsuki.springframework.beans.exception.BeanException;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.beans.factory.config.BeanFactoryPostProcessor;
import com.akitsuki.springframework.beans.factory.config.PropertyValue;
import com.akitsuki.springframework.beans.factory.config.PropertyValues;
import com.akitsuki.springframework.core.io.DefaultResourceLoader;
import com.akitsuki.springframework.core.io.Resource;import java.io.IOException;
import java.util.Properties;/*** @author ziling.wang@hand-china.com* @date 2022/12/9 9:59*/
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";private String location;@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {try {DefaultResourceLoader resourceLoader = new DefaultResourceLoader();Resource resource = resourceLoader.getResource(location);Properties properties = new Properties();properties.load(resource.getInputStream());String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();for (String beanName : beanDefinitionNames) {BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);PropertyValues propertyValues = beanDefinition.getPropertyValues();for(PropertyValue pv : propertyValues.getPropertyValues()) {Object value = pv.getValue();if (!(value instanceof String)) {continue;}String strValue = (String) value;StringBuilder sb = new StringBuilder(strValue);int startIndex = strValue.indexOf(DEFAULT_PLACEHOLDER_PREFIX);int endIndex = strValue.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);if (-1 != startIndex && -1 != endIndex && startIndex < endIndex) {String propKey = strValue.substring(startIndex + 2, endIndex);String propValue = properties.getProperty(propKey);sb.replace(startIndex, endIndex + 1, propValue);propertyValues.addPropertyValue(new PropertyValue(pv.getName(), sb.toString()));}}}} catch (IOException e) {throw new BeanException("加载配置时出错", e);}}public void setLocation(String location) {this.location = location;}
}

乍一看起来有些长,但实际上内容并不复杂,我们一点点来分析。

首先是前后缀,这个很好理解,用它来定位占位符。然后是一个location,这个location是指我们的配置文件所在地。要注意的是,这里的配置文件并不是指我们之前的spring.xml,而是我们的键值对配置文件,用来配置变量的值的文件。比如我们常用的 application.yml这样的文件。之后的内容都是后置处理器的内容,我们来详细分析。

第一段,通过我们之前实现的ResourceLoader,将配置文件中的内容,读取到Properties中。以便下面进行处理。之后,对Bean定义进行遍历,拿到Bean定义中的所有依赖属性,再对属性进行遍历,后面的内容看起来很长,其实是简单的字符串匹配、定位、替换的过程。将占位符替换成真正的值(存储在Properties中)后,再重新设置进Bean定义的PropertyValues中。这样,我们就完成了替换过程。

主角出场,自定义注解

接下来,我们要开始自定义注解的部分了。这次我们实现两个注解:@Scope注解和@Component注解。@Scope注解主要是用来表示Bean的作用域的,我们之前也介绍过关于单例Bean和非单例Bean的内容。而@Component注解,大家都非常熟悉了,我们用它来标识一个Bean。Spring中还有类似的@Controller、@Service等注解,但这些只是为了更加语义化,它们和@Component是等价的,我们这里就只实现@Component注解。

先来看@Scope注解

package com.akitsuki.springframework.context.annotation;import java.lang.annotation.*;/*** @author ziling.wang@hand-china.com* @date 2022/12/9 10:12*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {String value() default "singleton";
}

然后是@Component注解

package com.akitsuki.springframework.stereotype;import java.lang.annotation.*;/*** @author ziling.wang@hand-china.com* @date 2022/12/9 10:14*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {String value() default "";
}

注解本身没什么好说的,@Scope注解提供了一个属性,用于设置作用域。而@Component注解,设置的属性则是Bean的名称。

有了注解,我们要怎么使用呢?我们先来考虑@Component注解。hutool工具包给我们提供了一个方法,可以根据一个基本包路径和注解,扫描路径下所有包含这个注解的类。有了这个工具,我们就可以拿到这些类的信息,进而创建Bean定义了。

package com.akitsuki.springframework.context.annotation;import cn.hutool.core.util.ClassUtil;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.stereotype.Component;import java.util.LinkedHashSet;
import java.util.Set;/*** 通过注解扫描指定路径下的bean定义* @author ziling.wang@hand-china.com* @date 2022/12/9 10:15*/
public class ClassPathScanningCandidateComponentProvider {public Set<BeanDefinition> findCandidateComponents(String basePackage) {Set<BeanDefinition> candidates = new LinkedHashSet<>();Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackage, Component.class);for (Class<?> clazz : classes) {candidates.add(new BeanDefinition(clazz));}return candidates;}
}

嗯,非常的好用,如果没有这个 scanPackageByAnnotation方法,真不知道要怎么实现这个功能(笑)

但到这一步,我们只是实现了一个工具类而已。它只帮我们把Bean定义创建了出来,就没有下文了。所以我们要来继续完善这件事。

package com.akitsuki.springframework.context.annotation;import cn.hutool.core.util.StrUtil;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.akitsuki.springframework.stereotype.Component;
import lombok.AllArgsConstructor;import java.util.Set;/*** @author ziling.wang@hand-china.com* @date 2022/12/9 10:24*/
@AllArgsConstructor
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {private BeanDefinitionRegistry registry;public void doScan(String... basePackages) {for (String basePackage : basePackages) {Set<BeanDefinition> candidates = findCandidateComponents(basePackage);for (BeanDefinition beanDefinition : candidates) {String scope = resolveBeanScope(beanDefinition);if (StrUtil.isNotEmpty(scope)) {beanDefinition.setScope(scope);}registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);}}}/*** 处理bean的作用域* @param beanDefinition* @return*/private String resolveBeanScope(BeanDefinition beanDefinition) {Class<?> beanClass = beanDefinition.getBeanClass();Scope scope = beanClass.getAnnotation(Scope.class);if (null != scope) {return scope.value();}return StrUtil.EMPTY;}/*** 处理bean名称,以Component配置为准,如果没有配置,则取首字母小写的类名* @param beanDefinition* @return*/private String determineBeanName(BeanDefinition beanDefinition) {Class<?> beanClass = beanDefinition.getBeanClass();Component component = beanClass.getAnnotation(Component.class);String value = component.value();if (StrUtil.isEmpty(value)) {value = StrUtil.lowerFirst(beanClass.getSimpleName());}return value;}
}

又是个看起来挺长,没办法一眼就看懂的类。我们先从两个私有方法入手,这两个都是工具方法,分别用来处理作用域和Bean名称。我们先来看作用域,其实就是从Bean定义中拿到Class,再拿到@Scope注解,最后拿到注解里配置的值而已。如果没有注解或者是空,不要忘了我们的Bean定义会默认把作用域设置为单例,所以不用担心。接下来是处理Bean名称,和作用域类似,也是去拿注解的值。不同的是,这里如果没有拿到,那么就会用首字母小写的类名作为Bean名称。

然后我们看处理方法,其实也没什么好说的,只不过这里增加了多个包路径的循环操作,以及拿到了刚新建好的Bean定义,调用刚才的两个工具方法,设置作用域和Bean名称,最后注册到容器中。终于,在这一步,我们完成了注册的操作。

切入点:扫描配置

我们上面完成了这么多,但是,有这么一个问题:扫描的包地址,从哪里来。在Spring中,我们会在xml中配置component-scan,在其中配置我们要扫描的包路径。所以,久违的,我们要来扩充我们的xml解析功能了,加入对component-scan的解析操作。

package com.akitsuki.springframework.beans.factory.xml;/*** xml方式读取bean定义** @author ziling.wang@hand-china.com* @date 2022/11/9 14:18*/
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {/*** 真正通过xml读取bean定义的方法实现** @param inputStream xml配置文件输入流* @throws BeanException          e* @throws ClassNotFoundException e*/private void doLoadBeanDefinitions(InputStream inputStream) throws BeanException, ClassNotFoundException, DocumentException, SAXException {SAXReader reader = new SAXReader();//安全措施,防止xxe攻击reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);Document document = reader.read(inputStream);Element root = document.getRootElement();Element componentScan = root.element("component-scan");if (null != componentScan) {String scanPath = componentScan.attributeValue("base-package");if (StrUtil.isEmpty(scanPath)) {throw new BeanException("base package can not be null or empty");}scanPackage(scanPath);}//省略其他部分}private void scanPackage(String path) {String[] basePackages = StrUtil.splitToArray(path, ",");ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(getRegistry());scanner.doScan(basePackages);}
}

这里我们只截取了部分操作,而且,对整体的xml解析功能进行了一个升级,改用dom4j来进行操作,替换掉了原来的w3c系列工具。具体详细的代码可以去看我上传到gitee的代码。

这里的主体思想,就是从component-scan元素中,找到base-package属性,这里的base-package可以配置多个,用逗号进行分割。然后再调用我们上面所实现的scanner,将解析出来的包路径传入进去,完成Bean定义的解析注册。

测试

这次的内容,相对来说没有那么闹心,一切的实现都很轻松写意,毕竟物理学的大厦已经落成…(再次立flag)

这次,我们得再次请出我们的老朋友:UserDao和UserService了。这俩好兄弟,陪着我们走过了漫长的旅程,这次依然要为我们的测试,做出伟大的贡献。

package com.akitsuki.springframework.test.bean;import com.akitsuki.springframework.beans.factory.DisposableBean;
import com.akitsuki.springframework.beans.factory.InitializingBean;
import com.akitsuki.springframework.context.annotation.Scope;
import com.akitsuki.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;/*** @author ziling.wang@hand-china.com* @date 2022/11/8 14:42*/
@Component
@Scope("prototype")
public class UserDao implements InitializingBean, DisposableBean {private static final Map<Long, String> userMap = new HashMap<>();public String queryUserName(Long id) {return userMap.get(id);}@Overridepublic void destroy() throws Exception {System.out.println("执行UserDao的destroyMethod");userMap.clear();}@Overridepublic void afterPropertiesSet() throws Exception {System.out.println("执行UserDao的initMethod");userMap.put(1L, "akitsuki");userMap.put(2L, "toyosaki");userMap.put(3L, "kugimiya");userMap.put(4L, "hanazawa");userMap.put(5L, "momonogi");}
}

这次的UserDao,将为我们测试新的注解。相对的,我们的UserService则采用传统的方式

package com.akitsuki.springframework.test.bean;import com.akitsuki.springframework.beans.factory.DisposableBean;
import com.akitsuki.springframework.beans.factory.InitializingBean;
import com.akitsuki.springframework.context.ApplicationContext;
import lombok.Getter;
import lombok.Setter;/*** @author ziling.wang@hand-china.com* @date 2022/11/8 14:42*/
@Getter
@Setter
public class UserService implements InitializingBean, DisposableBean {private String dummyString;private int dummyInt;private UserDao userDao;public void queryUserInfo(Long id) {System.out.println("dummyString:" + dummyString);System.out.println("dummyInt:" + dummyInt);String userName = userDao.queryUserName(id);if (null == userName) {System.out.println("用户未找到>_<");} else {System.out.println("用户名:" + userDao.queryUserName(id));}}@Overridepublic void destroy() throws Exception {System.out.println("userService的destroy执行了");}@Overridepublic void afterPropertiesSet() throws Exception {System.out.println("userService的afterPropertiesSet执行了");}
}

嗯,基本内容都没啥变动,这里就不多介绍了

接下来,是我们的配置文件,这次我们不仅要有spring.xml,还要有application.yml

<?xml version="1.0" encoding="utf-8" ?>
<beans><component-scan base-package="com.akitsuki.springframework.test.bean" /><bean id="userService" class="com.akitsuki.springframework.test.bean.UserService"><property name="dummyString" value="${dummyString}"/><property name="dummyInt" value="${dummyInt}"/><property name="userDao" ref="userDao"/></bean><bean class="com.akitsuki.springframework.beans.factory.PropertyPlaceholderConfigurer"><property name="location" value="classpath:application.yml"/></bean>
</beans>
dummyString: kamisama
dummyInt: 114514

可以看到,我们在spring.xml中,配置了要扫描的包路径,userService也是用传统方法进行配置的,并且对于方法依赖的部分属性,也用了占位符来进行标记。然后在application.yml中,则是对占位符的变量进行了实际的配置。在我们对property解析的bean中,也将application.yml的路径,作为参数传了过去。

下面是主要测试类

package com.akitsuki.springframework.test;import com.akitsuki.springframework.context.support.ClasspathXmlApplicationContext;
import com.akitsuki.springframework.test.bean.UserService;
import org.junit.Test;/*** @author ziling.wang@hand-china.com* @date 2022/11/15 13:58*/
public class ApiTest {@Testpublic void test() {ClasspathXmlApplicationContext context = new ClasspathXmlApplicationContext("classpath:spring.xml");context.registerShutdownHook();UserService userService = context.getBean("userService", UserService.class);userService.queryUserInfo(1L);}
}

这个类,每次最轻松的就是它了,基本没啥大变化。

测试结果

执行UserDao的initMethod
userService的afterPropertiesSet执行了
dummyString:kamisama
dummyInt:114514
用户名:akitsuki
userService的destroy执行了Process finished with exit code 0

我们来分析一下这个结果,首先,可以看到UserDao被成功的创建了,证明我们的@Component注解生效了。然后只有UserService的destroy方法执行了,证明我们用@Scope注解,将UserDao的作用域修改为prototype也成功了。然后我们配置在application.yml中的变量,也成功的打印出来了,证明我们的占位符替换功能也成功了。这一次的练习,也圆满完成了。

相关源码可以参考我的gitee:https://gitee.com/akitsuki-kouzou/mini-spring,这里对应的代码是mini-spring-13

手写Spring-第十三章-超进化!用注解完成Bean的注册相关推荐

  1. 手写Spring-第十四章-再次进化!用注解完成属性的注入

    前言 上次我们的标题中用了[超进化]这个词,从配置文件升级到用注解来进行bean的注册,这确实可以称得上是超进化.但总觉得进化的不是那么完全,大概是从亚古兽进化到暴龙兽这样的程度(?).因为我们还是在 ...

  2. 手写Spring DI依赖注入,嘿,你的益达!

    手写DI 提前实例化单例Bean DI分析 DI的实现 构造参数依赖 一:定义分析 二:定义一个类BeanReference 三:BeanDefinition接口及其实现类 四:DefaultBean ...

  3. 手写 Spring 事务、IOC、DI 和 MVC

    Spring AOP 原理 什么是 AOP? AOP 即面向切面编程,利用 AOP 可以对业务进行解耦,提高重用性,提高开发效率 应用场景:日志记录,性能统计,安全控制,事务处理,异常处理 AOP 底 ...

  4. JAVA项目代码手写吗_一个老程序员是如何手写Spring MVC的

    见人爱的Spring已然不仅仅只是一个框架了.如今,Spring已然成为了一个生态.但深入了解Spring的却寥寥无几.这里,我带大家一起来看看,我是如何手写Spring的.我将结合对Spring十多 ...

  5. 记录一次阿里架构师全程手写Spring MVC

    人见人爱的Spring已然不仅仅只是一个框架了.如今,Spring已然成为了一个生态.但深入了解Spring的却寥寥无几.这里,我带大家一起来看看,我是如何手写Spring的.我将结合对Spring十 ...

  6. 十年java架构师分享:我是这样手写Spring的

    人见人爱的 Spring 已然不仅仅只是一个框架了.如今,Spring 已然成为了一个生态.但深入了解 Spring 的却寥寥无几.这里,我带大家一起来看看,我是如何手写 Spring 的.我将结合对 ...

  7. 从头开始实现一个小型spring框架——手写Spring之集成Tomcat服务器

    手写Spring之集成Tomcat与Servlet 写在前面 一.Web服务模型及servlet 1.1 Web服务器 1.2 请求流程 二.实现 三.小结 写在前面 最近学习了一下spring的相关 ...

  8. 手把手教你手写Spring框架

    手写spring准备工作: 新建一个maven工程: 架构 新建类: package com.spring;public class keweiqinApplicationContext {priva ...

  9. 手写 Spring MVC

    学习自<Spring 5核心原理与30个类手写实战>作者 Tom 老师 手写 Spring MVC 不多说,简历装 X 必备.不过练好还是需要求一定的思维能力. 一.整体思路 思路要熟练背 ...

最新文章

  1. 揭晓飞桨平台提速秘诀:INT8量化加速实现“事半功倍”
  2. 为什么Java要把字符串设计成不可变的
  3. Web开发者用什么编辑器?
  4. weblogic调优的经过
  5. XJOI 3585 The rescue plan 营救计划 题解
  6. 关于电脑的几十个单词及其缩写
  7. Eclipse导入Android项目 Eclipse常见错误 中文乱码问题
  8. android studio调整字体大小,如何在Android Studio中增加字体大小?
  9. 网络工程师(学习课件和视频)
  10. 牵引变压器短路阻抗定义及相关参数计算
  11. jumpserver-登录提示Server error occur, contact administrator
  12. 推荐两个适合程序员接国外私单的网站
  13. 四分位距IQR interquartile range
  14. 超时任务总结(tradingTask)
  15. python进程间通信之管道通信
  16. 汽车充电桩检测设备TK4860C交流充电桩检定装置
  17. “心脏滴血”漏洞复现
  18. 浙江公务员考试申论指导作答的思路与方法
  19. grub无法正常启动的解决方法
  20. python朴素贝叶斯对wine_基于朴素贝叶斯对Wine数据集分类

热门文章

  1. perl DBI使用详解
  2. Python2.7获取QQ好友头像
  3. firefox一些推荐的插件
  4. 在WordPress中使用Font Awesome
  5. 使用Python3批量保存贴吧图片-附爬虫程序
  6. 万有特性的概念与绘制
  7. 怎么使用手机便签软件将图片上的文字扫描出来
  8. linux 7zip 加密,7-Zip
  9. 第五家面试(东航电商 )
  10. 周小结——23.2.6