手撸Spring系列4:IOC/DI 思想(实战篇)

说在前头: 笔者本人为大三在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。

手撸Spring系列是笔者本人首次尝试的、较为规范的系列博客,将会围绕Spring框架分为 IOC/DI 思想Spring MVCAOP 思想Spring JDBC 四个模块,并且每个模块都会分为 理论篇源码篇实战篇 三个篇章进行讲解(大约12篇文章左右的篇幅)。从原理出发,深入浅出,一步步接触Spring源码并手把手带领大家一起写一个 迷你版的Spring框架 ,促进大家进一步了解Spring的本质!

由于源码篇涉及到源码的阅读,可能有小伙伴没有成功构建好Spring源码的阅读环境,笔者强烈建议:想要真正了解Spring,一定要构建好源码的阅读环境再进行研究,具体构建过程可查看笔者此前的博客:《如何构建Spring5源码阅读环境》

前言

经过前面两篇分别关于IOC和DI的源码篇博客后,终于迎来了我们 手撸Spring系列 的重头戏了——实战篇!

实战篇的源码我将会开源到码云 Gitee 上,仓库地址:https://gitee.com/bosen-once/mini-spring

实战篇的代码的重心会放在IOC/DI、MVC、AOP等思想的具体实现上,实现迷你版的Spring(除注解外,每个类对应的包与Spring源码保持一致),对于其扩展性不会做特别的考虑!!

那么废话不多说,直接上车吧!!!


一、常用注解的编写

在源码篇中,笔者通过AnnotationConfigApplicationContext来各位读者朋友解读的Spring源码,因此,实战篇中,笔者会先从使用最多、编写也最简单的注解类开始编写,包括(@Indexed@Component@Controller@Service@Repository@Autowired@ComponentScan)。

在正式开始编写前,还请各位读者朋友先看看整个程序的架构:

那么就让我们开始正式的编写吧!!


1.@Indexed

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>为Spring的模式注解添加索引</p>
 * @author Bosen
 * @date 2021/9/10 14:30
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Indexed {}


2.@Component

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>通用组件模式注解</p>
 * @author Bosen
 * @date 2021/9/10 14:31
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}


3.@Controller

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>Web控制器模式注解</p>
 * @author Bosen
 * @date 2021/9/10 14:19
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    String value() default "";
}


4.@Service

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>服务模式注解</p>
 * @author Bosen
 * @date 2021/9/10 14:18
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
    String value() default "";
}


5.@Repository

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>数据仓库模式注解</p>
 * @author Bosen
 * @date 2021/9/10 14:26
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
    String value() default "";
}


6.@Autowired

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>自动注入注解</p>
 * @author Bosen
 * @date 2021/9/10 14:23
 */
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    String value() default "";
}


7.@ComponentScan

package org.springframework.annotation;

import java.lang.annotation.*;

/**
 * <p>配置需要扫描的包</p>
 * @author Bosen
 * @date 2021/9/11 14:10
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ComponentScan {
    String value() default "";
}


二、beans模块的编写

1.BeanFactory

知识温故:理论篇源码篇中,我们反复强调了BeanFactroy是spring的顶层接口,定义了工厂的基本功能,而在我们的迷你版spring中,会将其简化为只有getBean方法的接口~

package org.springframework.beans.factory;

/**
 * <p>spring顶层接口</p>
 * @author Bosen
 * @date 2021/9/10 14:35
 */
public interface BeanFactory {
    /**
     * <p>通过bean名称获取bean实例</p>
     */
    Object getBean(String beanName);
}


2.DefaultListableBeanFactory

知识温故: DefaultListableBeanFactoryBeanFactory三个子类接口的默认实现类!(迷你版中,我们不编写这三个子接口,因此我们让该类直接继承BeanFactory即可)

package org.springframework.beans.factory.support;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>bean工厂的实现类</p>
 * @author Bosen
 * @date 2021/9/10 15:28
 */
public class DefaultListableBeanFactory implements BeanFactory {
    /**
     * <p>用于存放bd的map</p>
     */
    public final Map<String, BeanDefinition> beanDefinitionMap =
            new ConcurrentHashMap<>();

    @Override
    public Object getBean(String beanName) {
        return null;
    }
}


3.BeanDefinition

知识温故: BeanDefinition用于存储一个bean的信息,是bean原料

package org.springframework.beans.factory.config;

/**
 * <p>保存bean定义相关的信息</p>
 * @author Bosen
 * @date 2021/9/10 14:41
 */
public class BeanDefinition {
    /**
     * <p>bean对应的全类名</p>
     */
    private String beanClassName;

    /**
     * <p>是否懒加载</p>
     */
    private boolean lazyInit = false;

    /**
     * <p>保存在IOC容器时的key值</p>
     */
    private String factoryBeanName;

    public String getBeanClassName() {
        return beanClassName;
    }

    public void setBeanClassName(String beanClassName) {
        this.beanClassName = beanClassName;
    }

    public boolean isLazyInit() {
        return lazyInit;
    }

    public void setLazyInit(boolean lazyInit) {
        this.lazyInit = lazyInit;
    }

    public String getFactoryBeanName() {
        return factoryBeanName;
    }

    public void setFactoryBeanName(String factoryBeanName) {
        this.factoryBeanName = factoryBeanName;
    }
}


4.BeanWrapper

知识温故: BeanWrapper主要用于封装创建后的对象实例(bean)。

package org.springframework.beans;

/**
 * <p>bean的包装类</p>
 * @author Bosen
 * @date 2021/9/10 14:48
 */
public class BeanWrapper {
    /**
     * <p>回由该对象包装的bean实例</p>
     */
    private Object wrappedInstance;

    public BeanWrapper(Object wrappedInstance) {
        this.wrappedInstance = wrappedInstance;
    }

    /**
     * <p>返回包装的bean实例的类型</p>
     */
    private Class<?> wrappedClass;

    public Object getWrappedInstance() {
        return wrappedInstance;
    }

    public void setWrappedInstance(Object wrappedInstance) {
        this.wrappedInstance = wrappedInstance;
    }

    public Class<?> getWrappedClass() {
        return wrappedClass;
    }

    public void setWrappedClass(Class<?> wrappedClass) {
        this.wrappedClass = wrappedClass;
    }
}


5.BeanDefinitionReader

知识温故: BeanDefinitionReader从名称就知道是一个BeanDefinition的读取器,完成对BeanDefinition读取的具体工作!

package org.springframework.beans.factory.support;

import org.springframework.beans.factory.config.BeanDefinition;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>用于扫描bd</p>
 * @author Bosen
 * @date 2021/9/11 14:00
 */
public class BeanDefinitionReader {
    /**
     * <p>存储扫描出来的bean的全类名</p>
     */
    private List<String> registryBeanClasses = new ArrayList<>();

    public BeanDefinitionReader(String scanPackage) throws Exception {
        doScan(scanPackage);
    }

    /**
     * <p>扫描包下的类</p>
     * @param scanPackage 包名
     */
    public void doScan(String scanPackage) throws Exception {
        // 将包名转为文件路径
        URL url = this.getClass().getResource("/" + scanPackage.replaceAll("\\.", "/"));
        if (url == null) {
            throw new Exception("包" + scanPackage + "不存在!");
        }
        File classPath = new File(url.getFile());
        for (File file : classPath.listFiles()) {
            if (file.isDirectory()) {
                doScan(scanPackage + "." +file.getName());
            } else {
                if (!file.getName().endsWith(".class")) {
                    // 如果不是class文件则跳过
                    continue;
                }
                String className = scanPackage + "." + file.getName().replace(".class", "");
                registryBeanClasses.add(className);
            }
        }
    }

    /**
     * <p>将扫描到的类信息转化为bd对象</p>
     */
    public List<BeanDefinition> loadBeanDefinitions() {
        List<BeanDefinition> result = new ArrayList<>();
        try {
            for (String className : registryBeanClasses) {
                Class<?> beanClass = Class.forName(className);
                if (beanClass.isInterface()) {
                    // 如果是接口则跳过
                    continue;
                }
                result.add(doCreateBeanDefinition(toLowerFirstCase(beanClass.getSimpleName()), beanClass.getName()));

                Class<?>[] interfaces = beanClass.getInterfaces();
                for (Class<?> anInterface : interfaces) {
                    result.add(doCreateBeanDefinition(anInterface.getName(), beanClass.getName()));
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }
    
    /**
     * <p>将类信息转化为beanDefinition</p>
     */
    public BeanDefinition doCreateBeanDefinition(String factoryBeanName, String beanClassName) {
        BeanDefinition beanDefinition = new BeanDefinition();
        beanDefinition.setBeanClassName(beanClassName);
        beanDefinition.setFactoryBeanName(factoryBeanName);
        return beanDefinition;
    }
    
    /**
     * <p>将类名首字母小写</p>
     */
    private String toLowerFirstCase(String simpleName) {
        char[] chars = simpleName.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
}


三、context模块的编写

1.ApplicationContext

在迷你版中,我们只让ApplicationContext定义关键方法refresh即可。

package org.springframework.context;

import org.springframework.beans.factory.BeanFactory;

/**
 * <p>容器顶层接口</p>
 * @author Bosen
 * @date 2021/9/10 15:18
 */
public interface ApplicationContext extends BeanFactory {
    void refresh() throws Exception;
}


2.AbstractApplicationContext

将接口ApplicationContext定义完成后,紧接着的工作当然是编写其子类啦!~~在ApplicationContext众多子类中,完成主要工作的就是AbstractApplicationContext(IOC容器的创建、beanDefinition的扫描创建、bean的创建、依赖注入等都在这里完成)

注意:以下编写的代码只实现了IOC(控制反转)功能,并未实现DI(依赖注入)功能,DI功能的编写将放在文章后半段,前半段完成IOC的编写和测试!

package org.springframework.context.support;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;

import java.util.List;
import java.util.Map;

/**
 * <p>容器抽象类</p>
 * @author Bosen
 * @date 2021/9/10 15:19
 */
public abstract class AbstractApplicationContext extends DefaultListableBeanFactory implements ApplicationContext {

    protected BeanDefinitionReader reader;
	
	/**
     * <p>保存单例对象</p>
     */
    private Map<String, Object> factoryBeanObjectCache = new HashMap<>();

    /**
     * <p>保存包装对象</p>
     */
    private Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>();
	
    @Override
    public void refresh() throws Exception {
        // 扫描需要扫描的包,并把相关的类转化为beanDefinition
        List<BeanDefinition> beanDefinitions = reader.loadBeanDefinitions();
        // 注册,将beanDefinition放入IOC容器存储
        doRegisterBeanDefinition(beanDefinitions);
        // 将非懒加载的类初始化
        doAutowired();
    }

    /**
     * <p>将beanDefinition放入IOC容器存储</p>
     */
    private void doRegisterBeanDefinition(List<BeanDefinition> beanDefinitions) throws Exception {
        for (BeanDefinition beanDefinition : beanDefinitions) {
            if (super.beanDefinitionMap.containsKey(beanDefinition.getFactoryBeanName())) {
                throw new Exception(beanDefinition.getFactoryBeanName() + "已经存在!");
            }
            super.beanDefinitionMap.put(beanDefinition.getFactoryBeanName(), beanDefinition);
        }
    }

    /**
     * <p>将非懒加载的类初始化</p>
     */
    private void doAutowired() {
        for (Map.Entry<String, BeanDefinition> beanDefinitionEntry : super.beanDefinitionMap.entrySet()) {
            String beanName = beanDefinitionEntry.getKey();
            if (!beanDefinitionEntry.getValue().isLazyInit()) {
                getBean(beanName);
            }
        }
    }

    @Override
    public Object getBean(String beanName) {
        return null;
    }
}


3.AnnotationConfigApplicationContext

知识温故: 之前我们讲述了,ApplicationContext的子类是有多种的(迷你版中只实现一种),比如我们最熟知的ClassPathXmlApplicationContextAnnotationConfigApplicationContext,前者的配置基于xml文件,后者基于注解,他们对于配置的解读也会有不同的方式,但他们其他任务的处理流程基本都是一致的。因此,为了让我们编写的代码有更高的可用性,我们可以将他们不同部分的代码放在他们自己内部中完成,而相同逻辑部分的代码则交给他们共同的父类AbstractApplicationContext来完成。

弱弱的说一句:这不明摆着"坑爹"嘛~~!!

package org.springframework.context.annotation;

import org.springframework.annotation.ComponentScan;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.context.support.AbstractApplicationContext;

/**
 * <p>基于注解作为配置的容器</p>
 * @author Bosen
 * @date 2021/9/10 15:32
 */
public class AnnotationConfigApplicationContext extends AbstractApplicationContext {

    public AnnotationConfigApplicationContext(Class annotatedClass) throws Exception {
        // 初始化父类bdw
        super.reader = new BeanDefinitionReader(getScanPackage(annotatedClass));
        refresh();
    }

    @Override
    public void refresh() throws Exception {
        // 交给父类完成
        super.refresh();
    }

    /**
     * <p>获取@ComponentScan中的value值</p>
     */
    public String getScanPackage(Class annotatedClass) throws Exception {
        // 判断是否有ComponentScan注解
        if (!annotatedClass.isAnnotationPresent(ComponentScan.class)) {
            throw new Exception("请为注解配置类加上@ComponentScan注解!");
        }
        ComponentScan componentScan =
                (ComponentScan) annotatedClass.getAnnotation(ComponentScan.class);
        return componentScan.value().trim();
    }
}

至此,迷你版Spring的IOC功能已经实现了,接下来我们做一个测试看看Bean工厂是否可以正常运作!


四、迷你版IOC功能测试

测试很简单,只需要编写一个配置类ApplicationConfig并且加上注解@Component,以及随便的编写几个类即可(如:TestControllerTestServiceTestDAO

1.配置类ApplicationConfig

package org.springframework.test.config;

import org.springframework.annotation.ComponentScan;

/**
 * <p>配置类</p>
 * @author Bosen
 * @date 2021/9/11 14:11
 */
@ComponentScan("org.springframework.test")
public class ApplicationConfig {}


2.启动类ApplicationTest

package org.springframework.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.config.ApplicationConfig;

/**
 * <p>测试启动类</p>
 * @author Bosen
 * @date 2021/9/12 0:25
 */
public class ApplicationTest {
    public static void main(String[] args) throws Exception {
        ApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(ApplicationConfig.class);
        applicationContext.getBean("");
    }
}


3.开始迷你IOC的测试

在如下位置打个断点,并debug一下

断点运行后,查看控制台的信息,目光看到beanDefinition中,会发现,在配置类中配置的包org.springframework.test下的所有类都已经被注册为beanDefinition,并存入了beanDefinitionMap中。这表明,我们的IOC功能已经成功实现了!!

IOC功能完成后,我们将继续来完成接下来的DI功能~~!


五、实现DI依赖注入

在源码篇中,我们介绍了Spring DI的入口是ApplicationContest中调用的getBean方法,因此,我们迷你版Spring的DI实现也放到这里来实现。

回到上述的IOC实现中的AbstractApplicationContext重写其getBean方法,实现DI。(主要工作: 1.通过工厂中的beanDefinition实例化bean->2.将实例化后的bean使用beanWrapper包装->3.利用反射机制bean进行依赖注入的操作,具体代码如下)

    @Override
    public Object getBean(String beanName) {
        BeanDefinition beanDefinition = super.beanDefinitionMap.get(beanName);
        try {
            // 通过bd实例化bean
            Object instance = instantiateBean(beanDefinition);
            if (instance == null) {
                return null;
            }
            // 将实例化后的bean使用bw包装
            BeanWrapper beanWrapper = new BeanWrapper(instance);

            this.factoryBeanInstanceCache.put(beanDefinition.getBeanClassName(), beanWrapper);

            // 开始注入操作
            populateBean(instance);

            return instance;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * <p>通过bd,实例化bean</p>
     */
    private Object instantiateBean(BeanDefinition beanDefinition) {
        Object instance = null;
        String className = beanDefinition.getBeanClassName();
        try {
            // 先判断单例池中是否存在该类的实例
            if (this.factoryBeanObjectCache.containsKey(className)) {
                instance = this.factoryBeanObjectCache.get(className);
            } else {
                Class<?> clazz = Class.forName(className);
                instance = clazz.newInstance();

                this.factoryBeanObjectCache.put(beanDefinition.getFactoryBeanName(), instance);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return instance;
    }

    /**
     * <p>开始注入操作</p>
     */
    public void populateBean(Object instance) {
        Class clazz = instance.getClass();
        // 判断是否有Controller、Service、Component、Repository等注解标记
        if (!(clazz.isAnnotationPresent(Component.class) ||
                clazz.isAnnotationPresent(Controller.class) ||
                clazz.isAnnotationPresent(Service.class) ||
                clazz.isAnnotationPresent(Repository.class))) {
            return;
        }

        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            // 如果属性没有被Autowired标记,则跳过
            if (!field.isAnnotationPresent(Autowired.class)) {
                continue;
            }

            String autowiredBeanName = field.getType().getName();

            field.setAccessible(true);

            try {
                field.set(instance, this.factoryBeanInstanceCache.get(autowiredBeanName).getWrappedInstance());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

六、迷你版DI功能测试

1.TestDAO

编写一个DAO层对象用于测试

package org.springframework.test.dao;

import org.springframework.annotation.Repository;

/**
 * @author Bosen
 * @date 2021/9/11 22:29
 */
@Repository
public class TestDAO {
    public String echo() {
        return "This is TestDAO#echo!!!";
    }
}

2.TestService

编写一个service层对象用于测试,其中依赖DAO层对象TestDAO

package org.springframework.test.service;

import org.springframework.annotation.Autowired;
import org.springframework.annotation.Service;
import org.springframework.test.dao.TestDAO;

/**
 * @author Bosen
 * @date 2021/9/11 22:30
 */
@Service
public class TestService {
    @Autowired
    TestDAO testDAO;

    public void echo() {
        System.out.println(testDAO.echo());
    }
}


3.编写测试类

编写一个测试类,通过getBean方式获取service层的对象,并调用该对象的echo方法~~!

package org.springframework.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.config.ApplicationConfig;
import org.springframework.test.service.TestService;

/**
 * <p>测试启动类</p>
 * @author Bosen
 * @date 2021/9/12 0:25
 */
public class ApplicationTest {
    public static void main(String[] args) throws Exception {
        ApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(ApplicationConfig.class);

        TestService service = (TestService) applicationContext.getBean("testService");

        service.echo();
    }
}


4.开始测试

直接运行测试类中的main方法,查看控制台输出信息如下:

可以看到,在DAOecho方法中的字符串 “This is TestDAO#echo!!!” 已经通过service的调用成功输出了出来,表明testDAO对象已经成功的注入到testService对象中,我们的DI功能已经成功实现了~~!!

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 《Spring 技术实战4》是一本与Spring框架有关的技术实战书籍。本书内容覆盖了Spring框架的核心技术与应用场景,并且结合了实战案例,通过实际开发过程中遇到的问题及其解决方案,帮助读者更好地理解和掌握Spring框架。 本书具体包括Spring框架的基本概念、IOC(控制反转)和DI(依赖注入)、AOP(面向切面编程)技术、Spring MVC(模型视图控制器)的应用以及Spring Boot(快速构建Spring应用)的实践等方面的内容。此外,还涉及了数据库、事务、集成测试和安全认证等方面的内容。 本书是一本适合中级开发人员阅读的书籍,需要读者具备一定的Java技术基础,并且需要对Spring框架有一定的了解。在阅读本书时,读者可以通过跟随书中的案例代码,逐步深入学习Spring框架的相关技术,从而能够在实际项目中应用到所学的知识。 总之,《Spring 技术实战4》是一本权威的Spring框架实战教程,无论是对于想要深入学习Spring技术的Java开发人员还是对于正在实际开发项目中遇到Spring技术相关问题的开发人员都是一本必读的好书。 ### 回答2: Spring是一款流行的开源框架,它为Java应用程序开发提供了广泛的功能支持,包括依赖注入、面向切面编程、数据访问、集成测试等。在Spring实战4这本书中,作者通过实际示例和案例,深入浅出地介绍了使用Spring开发企业级Java应用程序的实践技巧和经验。 该书分为三个部分,每个部分都涵盖了一个不同的主题。第一部分介绍了Spring的基础知识和核心概念,例如依赖注入、AOP、声明式事务和Spring MVC。第二部分重点讲解了如何使用Spring开发企业级应用程序,包括数据访问、使用Spring集成与消息传递和使用Spring Boot构建可扩展的应用程序。第三部分则介绍了一些高级主题,例如Spring Security、Spring集成测试、Spring Rest和Spring WebFlux。 总的来说,这本书为初学者和有经验的开发人员提供了很多有价值的信息和实践经验,包括如何正确地使用Spring框架、如何构建一些流行的企业应用程序和如何解决开发过程中遇到的一些常见问题。从初学者到专家,每个读者都可以从这本书中找到对自己有用的信息。 ### 回答3: Spring技术实战(第4版)是一本全面介绍Spring框架的实战指南。这本书提供了丰富的例子和各种技术的实现方案,对于刚接触Spring框架的初学者来说尤其有用。本书第一部分重点介绍Spring的基础知识,包括SpringIOC和AOP原理、Spring MVC框架和Restful服务等。第二部分介绍了Spring的高阶应用,例如Spring与NoSQL数据库的结合、使用Spring实现分布式和云端应用等。本书还涵盖了最新的Spring 5框架,并介绍了一些新增的特性,例如响应式编程和函数式编程。此外,本书还包括了一些对于Java开发者来说非常有用的主题,例如Spring Boot、Spring Security和Spring Data等。总之,Spring技术实战(第4版)是一本非常棒的Spring框架教程和实战指南,可以帮助Java开发者更好地理解和应用Spring框架。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云丶言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值