手写Spring Config,最终一战,来瞅瞅撒!

狐言不胡言 2021-06-23 17:27:12
spring config 手写 最终 一战


image.png

上一篇说到了手写Spring AOP,来进行功能的增强,下面本篇内容主要是手写Spring Config。通过配置的方式来使用Spring

前面内容链接:

我自横刀向天笑,手写Spring IOC容器,快来Look Look!

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

手写Spring AOP,快来瞧一瞧看一看撒!

配置分析

为什么要提供配置的方式呢,之前的内容中我们测试的时候都是通过代码来进行的:

GeneralBeanDefinition bd = new GeneralBeanDefinition();
bd.setBeanClass(Lad.class);
List<Object> args = new ArrayList<>();
args.add("sunwukong");
args.add(new BeanReference("magicGril"));
bd.setConstructorArgumentValues(args);
bf.registerBeanDefinition("swk", bd);
bd = new GeneralBeanDefinition();
bd.setBeanClass(MagicGril.class);
args = new ArrayList<>();
args.add("baigujing");
bd.setConstructorArgumentValues(args);
bf.registerBeanDefinition("magicGril", bd);

下面看下平时使用的时候,通过配置是什么样的:

<?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="girl" class="di.MagicGirl"
init-method="start" destroy-method="end">
<constructor-arg type="java.lang.String" value="girl"></constructor-arg>
<property name="friend" ref="boy"></property>
</bean>
<bean id="boy" class="di.Lad">
<constructor-arg type="java.lang.String" value="boy"></constructor-arg>
<constructor-arg type="di.MagicGirl" value="girl"></constructor-arg>
</bean>
</beans>

可以看出,提供配置的方式的优点:

  • 实用简单,改动起来比较灵活
  • 而且不需要改动代码

常用的配置方式,就是XML和注解的形式,它们的工作过程如下:

image.png

配置的工作过程

定义XML标记和注解

需要定义什么样的XML标记和注解呢?通过之前的内容知道,配置的内容就是Bean定义信息,那么Bean定义的内容就是需要配置的内容

首先来看下Bean定义接口中有哪些信息:

image.png

XML配置的方式,首先需要定义一个DTD或者XSD文档,来定义一套标记信息,去指定Bean定义

<bean id="girl" class="di.MagicGirl"
init-method="start" destroy-method="end">
<constructor-arg type="java.lang.String" value="girl"></constructor-arg>
<property name="friend" ref="boy"></property>
</bean>

可以看出,bean的配置指定的内容就是Bean定义接口中的信息

注解的方式,需要定义一套注解,那么需要哪些注解呢,也是Bean定义接口中的内容:

  • 指定类、指定BeanName、指定scope、指定工厂方法、指定工厂Bean、指定init method、指定destroy method,这些在我们使用Spring的时候是通过@Component来实现的
  • 指定构造参数的依赖:@Autowired、@Qualifier
  • 指定属性依赖:@Value

Bean配置的解析

Bean配置的解析过程,需要单独的接口来实现,而不是在BeanFactory中来做,要做到单一职责原则,所以需要定义单独的接口来解析Bean配置,然后再向BeanFactory注册Bean定义

ApplicationContext接口

ApplicationContext这个接口就是用来完成Bean配置解析的,上面说到实现配置的方式有XML和注解,所以会有两个实现类来实现ApplicationContext接口

image.png

  1. XML方式的实现:
  • XML文件可能存在多个,所以这里使用了list
  • 需要完成:加载xml、解析xml、创建Bean定义、注册Bean定义的任务
  1. 注解方式的实现
  • 扫描的包也会存在多个,这里也使用list
  • 需要完成:扫描包、获取注解、创建Bean定义、注册Bean定义的任务

因为需要创建和注册Bean定义,所以会使用到BeanFactory和BeanDefinitionRegistry接口,那么这部分代码在子类中分别实现的话就会重复,所以抽象出来放在父类中:

image.png

用户在使用的使用需要知道哪些接口和类呢?

  1. 指定配置相关:xml、注解
  2. 获取bean相关:BeanFactory

那么可以使用外观模式,让用户只需要知道ApplicationContext和其子类就行了,ApplicationContext可以继承BeanFactory,继而把两个接口合在一起:

image.png

ApplicationContext接口:

/**
* @className: ApplicationContext
* 用来构建整个应用环境的接口,用来完成Bean的配置和解析
* 1:为了减少用户对框架类接口的依赖,扩展了BeanFactory接口,
* Bean的配置和Bean的获取都可以通过ApplicationContext接口来完成
* 2:配置资源的方式有xml和注解,所以存在xml和注解两种子类的实现
* 3. Bean配置解析首先需要加载,所以实现了配置资源Resource的加载接口ResourceLoader
* @author: TR
*/
public interface ApplicationContext extends ResourceLoader,BeanFactory {
}

ApplicationContext的抽象类实现

/**
* @className: AbstractApplicationContext
* @description: ApplicationContext的抽象类实现
* @author: TR
*/
public abstract class AbstractApplicationContext implements ApplicationContext {
/** 用组合的方式来持有BeanFactory,完成BeanFactory接口的方法 */
protected BeanFactory beanFactory;
public AbstractApplicationContext() {
super();
this.beanFactory = new PreBuildBeanFactory();
}
public AbstractApplicationContext(BeanFactory beanFactory) {
super();
this.beanFactory = beanFactory;
}
@Override
public Object getBean(String beanName) throws Exception {
return this.beanFactory.getBean(beanName);
}
@Override
public void registerBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
this.beanFactory.registerBeanPostProcessor(beanPostProcessor);
}
}

xml配置方式的ApplicationContext实现类

/**
* @className: XmlApplicationContext
* @description: xml配置方式的ApplicationContext实现类
* @author: TR
*/
public class XmlApplicationContext extends AbstractApplicationContext {
}

注解配置方式的ApplicationContext实现类

/**
* @className: AnnotationApplicationContext
* @description: 注解配置方式的ApplicationContext实现类
* @author: TR
*/
public class AnnotationApplicationContext extends AbstractApplicationContext {
}

配置的实现

XML方式

XML文件来源的处理

xml配置文件的来源会有多种,比如:

image.png

不同来源的XML文件,它的加载方式是不一样的,但是在解析的过程中,最后都希望获取到InputStream

这里也需要设计一套接口,对于不同来源的XML文件分别进行处理

image.png

InputStreamSource接口

/**
* @className: InputStreamSource
* @description: 配置方式的最终统一接口
* @author: TR
*/
public interface InputStreamSource {
/**
* 最终要获取的就是输入流
* @return: java.io.InputStream
**/
InputStream getInputStream() throws IOException;
}

Resource接口

/**
* @className: Resource
* @description: 输入流的资源扩展接口
* @author: TR
*/
public interface Resource extends InputStreamSource {
//classpath形式的xml配置文件
String CLASS_PATH_PREFIX = "classpath:";
//系统文件形式的xml配置文件
String File_SYSTEM_PREFIX = "file:";
/**
* 判断资源是否存在
* @return: boolean
**/
boolean exists();
/**
* 是否可读
* @return: boolean
**/
boolean isReadable();
/**
* 是否打开
* @return: boolean
**/
boolean isOpen();
/**
* 获取资源文件
* @return: java.io.File
**/
File getFile();
}

InputStreamSource接口的实现类

FileSystemResource实现类:

/**
* @className: FileSystemResource
* @description: 系统文件类型的资源实现类
* @author: TR
*/
public class FileSystemResource implements Resource {
/** 文件资源对象 */
private File file;
public FileSystemResource(String fileName) {
super();
this.file = new File(fileName);
}
public FileSystemResource(File file) {
super();
this.file = file;
}
@Override
public boolean exists() {
return this.file == null ? false : this.file.exists();
}
@Override
public boolean isReadable() {
return this.file == null ? false : this.file.canRead();
}
@Override
public boolean isOpen() {
return false;
}
@Override
public File getFile() {
return file;
}
@Override
public InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
}

ClassPathResource实现类:

/**
* @className: ClassPathResource
* @description: classpath形式的资源实现类
* @author: TR
*/
public class ClassPathResource implements Resource {
//classpath所需要的信息
private String path;
private Class<?> clazz;
private ClassLoader classLoader;
public ClassPathResource(String path) {
this(path, null );
}
public ClassPathResource(String path, Class<?> clazz) {
this(path, clazz, null);
}
public ClassPathResource(String path, Class<?> clazz, ClassLoader classLoader) {
super();
this.path = path;
this.clazz = clazz;
this.classLoader = classLoader;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public Class<?> getClazz() {
return clazz;
}
public void setClazz(Class<?> clazz) {
this.clazz = clazz;
}
public ClassLoader getClassLoader() {
return classLoader;
}
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public boolean exists() {
if (StringUtils.isNotBlank(path)) {
if (this.clazz != null) {
return this.clazz.getResource(path) != null;
}
if (this.classLoader != null) {
return this.classLoader.getResource(path.startsWith("/") ? path.substring(1) : path) != null;
}
return this.getClass().getResource(path) != null;
}
return false;
}
@Override
public boolean isReadable() {
return exists();
}
@Override
public boolean isOpen() {
return false;
}
@Override
public File getFile() {
return null;
}
@Override
public InputStream getInputStream() throws IOException {
if (StringUtils.isNotBlank(path)) {
if (this.clazz != null) {
return this.clazz.getResourceAsStream(path);
}
if (this.classLoader != null) {
return this.classLoader.getResourceAsStream(path.startsWith("/") ? path.substring(1) : path);
}
return this.getClass().getResourceAsStream(path);
}
return null;
}
}

UrlResource实现类:

/**
* @className: UrlResource
* @description: URL形式的资源实现类
* @author: TR
*/
public class UrlResource implements Resource {
/** url的资源对象 */
private URL url;
public UrlResource(String url) throws IOException {
this.url = new URL(url);
}
public UrlResource(URL url) {
super();
this.url = url;
}
public URL getUrl() {
return url;
}
public void setUrl(URL url) {
this.url = url;
}
@Override
public boolean exists() {
return this.url != null;
}
@Override
public boolean isReadable() {
return exists();
}
@Override
public boolean isOpen() {
return false;
}
@Override
public File getFile() {
return null;
}
@Override
public InputStream getInputStream() throws IOException {
return null;
}
}

XML资源加载器

用户给定资源时是一个字符串,上面有三种资源,那么谁去负责创建这些资源呢

这里需要定义一个资源加载器,去分辨不同的资源,然后进行加载,这部分工作是由ApplicationContext来完成的,所以ApplicationContext需要继承ResourceLoader接口

ResourceLoader接口:

/**
* @className: ResourceLoader
* 配置资源加载接口
* 不同的配置方式,加载过程不一样,所以需要抽象出来一个接口应对变化的部分
* 虽然加载的方式不一样,但是返回的资源结果是一样的,都是Resource
* @author: TR
*/
public interface ResourceLoader {
/**
* 加载资源
* @param location:
* @return: demo.context.Resource
**/
Resource getResource(String location) throws IOException;
}

在这里,还需要区分用户给的字符串代表的是哪种资源,所以需要定义字符串的规则:

image.png

注解方式

如何扫描的

扫描的包有哪些呢?

需要到指定的包目录下找出所有的类文件,而且要包含子孙包下的

image.png

需要定义一个资源路径的匹配行为

扫描的结果

扫描到了包下的class文件后,需要的是类名,而且扫描的是class文件,直接使用上面的FileResource即可

扫描的类ClassPathBeanDefinitionScanner

/**
* @className: ClassPathBeanDefinitionScanner
* @description: 扫描class文件
* @author: TR
*/
public class ClassPathBeanDefinitionScanner {
private static Log logger = LogFactory.getLog(ClassPathBeanDefinitionScanner.class);
private BeanDefinitionRegistry registry;
private BeanDefinitionReader reader;
private PathMatcher pathMatcher = new AntPathMatcher();
private String resourcePatter = "**/*.class";
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super();
this.registry = registry;
this.reader = new AnnotationBeanDefinitionReader(registry);
}
/**
* 扫描包的方法
* @param basePackages:
* @return: void
**/
public void scan(String... basePackages) throws Throwable {
if (basePackages != null && basePackages.length > 0) {
for (String b : basePackages) {
this.reader.loadBeanDefintions(doScan(b));
}
}
}
/**
* 将扫描的class转为Resource
* @param basePackage:
* @return: demo.context.Resource[]
**/
private Resource[] doScan(String basePackage) throws IOException {
// 扫描包下的类
// 构造初步匹配模式串,= 给入的包串 + / + **/*.class,替换里面的.为/
String pathPattern = StringUtils.replace(basePackage, ".", "/") + "/" + this.resourcePatter;
if (pathPattern.charAt(0) != '/') {
pathPattern = "/" + pathPattern;
}
// 找出模式的根包路径
String rootPath = this.determineRootDir(pathPattern);
// 得到文件名匹配的绝对路径模式
String fullPattern = this.getClass().getResource("/").toString() + pathPattern;
// 根据根包理解得到根包对应的目录
File rootDir = new File(this.getClass().getResource(rootPath).toString());
// 存放找到的类文件的resource集合
Set<Resource> scanedClassFileResources = new HashSet<>();
// 调用doRetrieveMatchingFiles来扫描class文件
this.doRetrieveMatchingFiles(fullPattern, rootDir, scanedClassFileResources);
return (Resource[]) scanedClassFileResources.toArray();
}
private String determineRootDir(String location) {
int rootDirEnd = location.length();
rootDirEnd = location.indexOf('*');
int zi = location.indexOf('?');
if (zi != -1 && zi < rootDirEnd) {
rootDirEnd = location.lastIndexOf('/', zi);
}
if (rootDirEnd != -1) {
return location.substring(0, rootDirEnd);
} else {
return location;
}
}
/**
* 递归找指定目录下的所有类,匹配模式的加入到结果中。
*
* @param fullPattern
* @param dir
* @param result
* @throws IOException
*/
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<Resource> result) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Searching directory [" + dir.getAbsolutePath() + "] for files matching pattern ["
+ fullPattern + "]");
}
for (File content : listDirectory(dir)) {
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
if (!content.canRead()) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping subdirectory [" + dir.getAbsolutePath()
+ "] because the application is not allowed to read the directory");
}
} else {
doRetrieveMatchingFiles(fullPattern, content, result);
}
}
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(new FileSystemResource(content));
}
}
}
protected File[] listDirectory(File dir) {
File[] files = dir.listFiles();
if (files == null) {
if (logger.isInfoEnabled()) {
logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
}
return new File[0];
}
Arrays.sort(files, Comparator.comparing(File::getName));
return files;
}
public BeanDefinitionRegistry getRegistry() {
return registry;
}
public void setRegistry(BeanDefinitionRegistry registry) {
this.registry = registry;
}
public BeanDefinitionReader getReader() {
return reader;
}
public void setReader(BeanDefinitionReader reader) {
this.reader = reader;
}
public PathMatcher getPathMatcher() {
return pathMatcher;
}
public void setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
}
public String getResourcePatter() {
return resourcePatter;
}
public void setResourcePatter(String resourcePatter) {
this.resourcePatter = resourcePatter;
}
}

解析成Bean定义

XML和注解最终的输出结果都是Resource,在这里还需要把Resource解析成Bean定义信息才行

需要定义接口来进行解析:

image.png

BeanDefinitionReader接口:

/**
* @className: BeanDefinitionReader
* @description: 将Resource资源解析成Bean定义的接口
* @author: TR
*/
public interface BeanDefinitionReader {
/**
* 解析单个资源
* @param resource:
* @return: void
**/
void loadBeanDefintions(Resource resource) throws Throwable;
/**
* 解析多个资源
* @param resource:
* @return: void
**/
void loadBeanDefintions(Resource... resource) throws Throwable;
}

AbstractBeanDefinitionReader抽象类:

/**
* @className: AbstractBeanDefinitionReader
* @description: TODO
* @date: 2021/6/10 15:58
* @author: jinpeng.sun
*/
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {
/** 持有BeanDefinitionRegistry接口,以便完成注册到BeanFactory中 */
protected BeanDefinitionRegistry beanDefinitionRegistry;
public AbstractBeanDefinitionReader(BeanDefinitionRegistry beanDefinitionRegistry) {
super();
this.beanDefinitionRegistry = beanDefinitionRegistry;
}
}

xml配置方式的bean定义解析器:

/**
* @className: XmlBeanDefinitionReader
* @description: xml配置方式的bean定义解析器
* @author: TR
*/
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
public XmlBeanDefinitionReader(BeanDefinitionRegistry beanDefinitionRegistry) {
super(beanDefinitionRegistry);
}
@Override
public void loadBeanDefintions(Resource resource) throws Throwable {
this.loadBeanDefintions(new Resource[] {resource});
}
@Override
public void loadBeanDefintions(Resource... resource) throws Throwable {
if (resource != null && resource.length > 0) {
for (Resource r : resource) {
this.parseXml(r);
}
}
}
private void parseXml(Resource r) {
//TODO 解析xml文档,获取bean定义,创建bean定义对象,注册到BeanDefinitionRegistry中
}
}

注解配置方式的bean定义解析器:

 * @className: AnnotationBeanDefinitionReader
* @description: 注解配置方式的bean定义解析器:
* @author: TR
*/
public class AnnotationBeanDefinitionReader extends AbstractBeanDefinitionReader {
public AnnotationBeanDefinitionReader(BeanDefinitionRegistry beanDefinitionRegistry) {
super(beanDefinitionRegistry);
}
@Override
public void loadBeanDefintions(Resource resource) throws Throwable {
this.loadBeanDefintions(new Resource[] {resource});
}
@Override
public void loadBeanDefintions(Resource... resource) throws Throwable {
if (resource != null && resource.length > 0) {
for (Resource r : resource) {
this.retriveAndRegistBeanDefinition(r);
}
}
}
private void retriveAndRegistBeanDefinition(Resource resource) {
if(resource != null && resource.getFile() != null) {
String className = getClassNameFormFile(resource.getFile());
try {
Class<?> clazz = Class.forName(className);
Component component = clazz.getAnnotation(Component.class);
if (component != null) {
GeneralBeanDefinition beanDefinition = new GeneralBeanDefinition();
beanDefinition.setBeanClass(clazz);
beanDefinition.setScope(component.scope());
beanDefinition.setFactoryMethodName(component.factoryMethodName());
beanDefinition.setFactoryBeanName(component.factoryBeanName());
beanDefinition.setInitMethodName(component.initMethodName());
beanDefinition.setDestroyMethodName(component.destroyMethodName());
//获取所有的构造方法,在构造方法上找Autowired注解,如果有的话,将这个构造方法set到bd
this.handleConstructor(clazz, beanDefinition);
//处理工厂方法参数依赖
if(StringUtils.isNotBlank(beanDefinition.getFactoryMethodName())) {
this.handleFactoryMethodArgs(clazz, beanDefinition);
}
//处理属性依赖
this.handlePropertyDi(clazz, beanDefinition);
String beanName = "".equals(component.value()) ? component.name() : null;
if (StringUtils.isBlank(beanName)) {
// TODO 应用名称生成规则生成beanName;
// 默认驼峰命名法
beanName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, clazz.getSimpleName());
}
// 注册bean定义
this.beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);
}
} catch (ClassNotFoundException | BeanDefinitionException e) {
e.printStackTrace();
}
}
}
private void handlePropertyDi(Class<?> clazz, GeneralBeanDefinition bd) {
// TODO Auto-generated method stub
}
private void handleFactoryMethodArgs(Class<?> clazz, GeneralBeanDefinition bd) {
// TODO Auto-generated method stub
}
private void handleConstructor(Class<?> clazz, GeneralBeanDefinition bd) {
//获取所有的构造方法,在构造方法上找Autowired注解,如果有的话,将这个构造方法set到bd
Constructor<?>[] constructors = clazz.getConstructors();
if (constructors != null && constructors.length > 0) {
for (Constructor c : constructors) {
if (c.getAnnotation(Autowired.class) != null) {
bd.setConstructor(c);
Parameter[] ps = c.getParameters();
//遍历获取参数上的注解,及创建参数依赖
break;
}
}
}
}
private int classPathAbsLength = AnnotationBeanDefinitionReader.class.getResource("/").toString().length();
private String getClassNameFormFile(File file) {
//返回绝对路径名字符串
String absPath = file.getAbsolutePath();
String name = absPath.substring(classPathAbsLength+1, absPath.indexOf("."));
return StringUtils.replace(name, File.separator, ".");
}
}

完善XmlApplicationContext和AnnotationApplicationContext:

public class XmlApplicationContext extends AbstractApplicationContext {
private List<Resource> resources;
private BeanDefinitionReader definitionReader;
public XmlApplicationContext(String... locations) throws Throwable {
super();
load(locations);
//资源解析成BeanDefinition,外派给BeanDefinitionReader接口来实现
this.definitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) this.beanFactory);
Resource[] resourceArray = new Resource[resources.size()];
resources.toArray(resourceArray);
//将解析后的BeanDefinition装载到BeanFactory中
definitionReader.loadBeanDefintions(resourceArray);
}
/**
* 根据用户指定的配置文件位置,加载资源信息
* @param locations:
* @return: void
**/
private void load(String[] locations) throws IOException {
if (resources == null) {
resources = new ArrayList<Resource>();
}
//完成加载,创建好Resource
if (locations != null && locations.length > 0) {
for (String lo : locations) {
Resource resource = getResource(lo);
if (resource != null) {
this.resources.add(resource);
}
}
}
}
@Override
public Resource getResource(String location) throws IOException {
if (StringUtils.isNotBlank(location)) {
//根据字符串的前缀判断区分,class、系统文件、url三种资源的加载
if (location.startsWith(Resource.CLASS_PATH_PREFIX)) {
return new ClassPathResource(location.substring(Resource.CLASS_PATH_PREFIX.length()));
} else if (location.startsWith(Resource.File_SYSTEM_PREFIX)) {
return new FileSystemResource(location.substring(Resource.File_SYSTEM_PREFIX.length()));
} else {
return new UrlResource(location);
}
}
return null;
}
}
public class AnnotationApplicationContext extends AbstractApplicationContext {
private ClassPathBeanDefinitionScanner scanner;
public AnnotationApplicationContext(String... locations) throws Throwable {
scanner = new ClassPathBeanDefinitionScanner((BeanDefinitionRegistry) this.beanFactory);
scanner.scan(locations);
}
@Override
public Resource getResource(String location) throws IOException {
return null;
}
}
版权声明
本文为[狐言不胡言]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/fox2said/p/14923650.html

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云