仓库地址

w4ngzhen/springboot-simple-guide: This is a project that guides SpringBoot users to get started quickly through a series of examples (github.com)

Chapter00 学习SpringBoot前的基本知识

一 反射

在Java中,我们可以通过反射(Reflection)来获取任何类的类型信息,其中最值得关注的就是Class类。通过Class类,我们拿到任意对象的相关上下文。

例如,我们有一个User类,代码如下:

  1. public class User {
  2. private String name;
  3. private int age;
  4. public User() {
  5. System.out.println("User 无参构造函数");
  6. }
  7. public User(String name, int age) {
  8. this.name = name;
  9. this.age = age;
  10. System.out.printf("User 有参构造函数:name = %s, age = %d%n", name, age);
  11. }
  12. // 忽略一堆的getter、setter
  13. }

如下的代码中,我们可以获取类上的声明的字段和各种方法:

  1. Class<User> userClass = User.class;
  2. System.out.println(userClass.getCanonicalName());
  3. // 1. 显示声明的字段
  4. Field[] fields = userClass.getDeclaredFields();
  5. System.out.println("该类包含了如下的字段:");
  6. for (Field field : fields) {
  7. System.out.println(field);
  8. }
  9. // 2. 显示声明的方法
  10. Method[] methods = userClass.getDeclaredMethods();
  11. System.out.println("该类包含了如下的方法:");
  12. for (Method method : methods) {
  13. System.out.println(method);
  14. }

当然,我们同样可以拿到构造函数,并通过反射的方式进行实例创建:

  1. // 3. 展示类上的构造函数对象
  2. Constructor<?>[] constructors = userClass.getConstructors();
  3. for (Constructor<?> constructor : constructors) {
  4. System.out.println("找到构造函数:" + constructor);
  5. }
  6. // 4. 通过无参构造函数创建实例
  7. Constructor<?> constructor = userClass.getConstructor();
  8. Object user1 = constructor.newInstance();
  9. System.out.println(user1);
  10. // 5. 通过有参构造函数创建实例
  11. // 注意有参构造函数获取时,传入了参数的class对象
  12. // 以及在newInstance的时候,需要传入实际的值
  13. Constructor<?> constructor1 = userClass.getConstructor(String.class, int.class);
  14. Object user2 = constructor1.newInstance("Mr.Hello", 18);
  15. System.out.println(user2);

在上述代码中,我们使用代码符号的方式获取的对应类的Class对象:

  1. Class<User> userClass = User.class;

这种情况下,我们必须能拿到User的符号。但更多的情况下,我们并没有类符号,反射允许我们通过类型的名称来进行:

  1. Class<?> userClass = Class.forName("com.compilemind.guide.chapter00.manual.User");

此外,我们还可以拿到类上的注解。首先我们定义一个注解:

  1. @Target(ElementType.TYPE) // 放在类型上
  2. @Retention(RetentionPolicy.RUNTIME) // 运行时保留
  3. public @interface UserInfo {
  4. String name();
  5. int age();
  6. }

然后给User上添加这个注解:

  1. @UserInfo(name = "annotationName", age = 99) // 使用注解
  2. public class User {
  3. // ...
  4. public User(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. System.out.printf("User 有参构造函数:name = %s, age = %d%n", name, age);
  8. }
  9. // ...
  10. }

利用反射,我们可以获取注解,并通过注解的方式,创建我们的User对象:

  1. // 1. 获取Class类对象
  2. Class<?> userClass = Class.forName(
  3. "com.compilemind.guide.chapter00.manual.User");
  4. // 2. 获取类上的 @UserInfo 注解
  5. UserInfo userInfo = userClass.getAnnotation(UserInfo.class);
  6. if (userInfo == null) {
  7. System.out.println(userClass.getCanonicalName() + "不包含注解" + UserInfo.class + ",终止创建");
  8. return;
  9. }
  10. // 3. 获取注解信息
  11. String name = userInfo.name();
  12. int age = userInfo.age();
  13. // 4. 通过有参构造函数,结合注解上的上下文,创建实例
  14. Object user = userClass.getConstructor(String.class, int.class).newInstance(name, age);
  15. System.out.println(user);

该部分可以在chapter00.manual包下查看源码。

二 Spring"构造"对象

可能读者会疑惑,为什么1.反射2.Spring"构造"对象会放在一起,实际上Spring的对象构造的底层就是使用反射进行的。接下来,让我们看看Spring中,如何"构造"一个对象。

编写一个新的示例UserEx:

  1. @Component // 使用注解,标记该类为一个组件
  2. public class UserEx {
  3. public UserEx() {
  4. System.out.println("UserEx 无参构造函数");
  5. }
  6. }

在这个UserEx中,我们在类上添加了注解@Component,标记该类为一个组件。然后创建一个新的类:IocApp,编写如下的代码:

  1. /**
  2. * "@SpringBootApplication" 标记是一个SpringBoot应用
  3. * 启动后,SpringBoot框架会去扫描当前包以及子包下(默认情况)的所有具有@Component标记的类,
  4. * 并通过反射的方式创建这个类的实例,存放在Spring的Bean容器中。
  5. */
  6. @SpringBootApplication //
  7. public class IocApp {
  8. public static void main(String[] args) {
  9. // 1. 启动
  10. ConfigurableApplicationContext context =
  11. SpringApplication.run(IocApp.class, args);
  12. // 2. 类符号获取
  13. System.out.println("通过类符号获取Bean");
  14. UserEx userEx = context.getBean(UserEx.class);
  15. System.out.println(userEx);
  16. // 3. 通过Bean的名称获取
  17. System.out.println("通过类符号获取Bean");
  18. UserEx userEx2 = (UserEx) context.getBean("userEx");
  19. System.out.println(userEx2);
  20. }
  21. }

在这段代码中,在有main函数的类上,添加@SpringBootApplication,标记是一个SpringBoot应用。

接着,我们在main函数中调用SpringApplication.run(IocApp.class, args);来启动这个SpringBoot应用。启动后,SpringBoot框架会去扫描当前包以及子包下(默认情况)的所有具有@Component标记的类,并通过反射的方式创建这个类的实例,存放在Spring的Bean容器中。

最后,我们通过调用ConfigurableApplicationContext.getBean来获取实例并进行打印。在这里,我们使用了两种方式来获取Bean:

  1. // 传入类符号.class
  2. <T> T getBean(Class<T> requiredType) throws BeansException;
  3. // 传入Bean的名称
  4. Object getBean(String name) throws BeansException;

这里简单提一下,Bean的name是有一定的规则。默认情况下,是类名称的小驼峰形式,这里UserEx对应的名称就是userEx;但是我们通过设置注解的name字段:@Component("myUserEx"),能够自定义在Bean在容器中的名称。

看到这里,让我们再次回顾第一节反射中的操作:我们在User类上添加注解@UserInfo,然后通过反射,获取注解的信息,并创建User实例。

那么现在,各位读者能否将第二节上述的内容,和反射中的操作关联起来呢?如果你能够大致理解我现在讲的含义,那么恭喜你,对于Spring进行对象构建的内部原理你已经有了一个简单的认识。

2.1.IOC控制反转

细心的读者已经发现了,在上一节中,我们创建了包含启动代码的类:IocApp。没错,此IOC就是这一节要讲的IOC(Inversion of Control),控制反转。

  传统的创建对象的方法是直接通过 new 关键字,而 spring 则是通过 IOC 容器来创建对象,也就是说我们将创建对象的控制权交给了 IOC 容器。我们可以用一句话来概括 IOC:

  IOC 让程序员不在关注怎么去创建对象,而是关注与对象创建之后的操作,把对象的创建、初始化、销毁等工作交给spring容器来做。

具体结合前面的例子来说,对于UserEx类,在没有IOC思想的介入下,我们创建这个UserEx类通常会这样做:

  1. UserEx userEx = new UserEx();
  2. // ... 使用该实例

而在Spring IOC框架下,我们做了如下的工作:

  1. 编写UserEx类,标注@Component
  2. 初始化容器:SpringApplication.run(...)
  3. 从容器中获取:UserEx userEx = context.getBean(UserEx.class);

看到这里,你也许会想没有IOC的模式下,我只要一行代码,而现在使用了Spring的IOC,多了这么多的配置和操作。难道不是更加的麻烦了吗?对于这个例子,的确是这样的,但是仔细一想,随着项目的体积逐渐增大,越来越多的类实例需要被创建,难道那个时候我们还要如此繁杂的通过new创建一大堆实例吗?另外,IOC容器帮我们做的事情,还远远不止控制反转这一项,依赖注入(dependence injection)也是一个重要的能力。

2.2.DI依赖注入

说到依赖注入,我们首先需要明确,在代码中什么是依赖。从互联网上有这样一段对于依赖的定义,我觉得很好:

每个软件,都是由很多“组件”构成的。这里的“组件”是指广义的组件 —— 组成部件,它可能是函数,可能是类,可能是包,也可能是微服务。软件的架构,就是组件以及组件之间的关系。而这些组件之间的关系,就是(广义的)依赖关系。

从狭义来讲,我们定义一个类GameEx,这GameEx包含前文的UserEx,我们就可以认为GameEx依赖UserEx:

  1. public class GameEx {
  2. private UserEx userEx; // GameEx依赖UserEx
  3. public void setUserEx(UserEx userEx) {
  4. this.userEx = userEx;
  5. System.out.println("调用setUserEx");
  6. }
  7. /**
  8. * 打印GameEx的UserEx
  9. */
  10. public void printUserEx() {
  11. if (this.userEx == null) {
  12. System.out.println("无用户");
  13. } else {
  14. System.out.println(this.userEx);
  15. }
  16. }
  17. }

在上述GameEx类中,拥有一个UserEx类型的字段userEx。同时,包含一个名为printUserEx的方法,用以打印UserEx实例。为了避免得到输出"无用户",我们需要在调用该方法前,设置UserEx的实例:

  1. // 伪代码
  2. UserEx userEx = ...; // 1.通过某种方式,获得的UserEx的实例
  3. GameEx gameEx = ...; // 2.通过某种方式,获得的GameEx的实例
  4. gameEx.setUserEx(userEx); // 3.调用setter方法将UserEx实例设置到GameEx中
  5. gameEx.printUserEx(); // 4.输出: UserEx@xxxx

在上面伪代码的第3步中,我们通过代码的方式手动调用setter函数。这个过程,本质上来讲,就是我们在控制着依赖:因为我们明白GameEx的功能依赖于UserEx,所以为了达到预期的目的,我们需要手动进行代码编写,处理这样的依赖。

让我们再看上述伪代码的中的第1、2步:得到UserEx和GameEx实例。在第2节中,我们已经学会了如何使用Spring的IOC容器创建对象,所以对于GameEx类,我们同样可以增加注解@Component,将GameEx标记为Bean,让Spring的IOC容器管理起来:

  1. @Component
  2. public class GameEx {
  3. // 其他代码忽略 ...
  4. }

然后,我们在IocApp中实现上述的伪代码效果:

  1. @SpringBootApplication //
  2. public class IocApp {
  3. public static void main(String[] args) {
  4. // 1. 启动
  5. ConfigurableApplicationContext context =
  6. SpringApplication.run(IocApp.class, args);
  7. manualDependencySet(context);
  8. }
  9. /**
  10. * 第2.2节伪代码实现:手动进行依赖设置
  11. */
  12. private static void manualDependencySet(ConfigurableApplicationContext context) {
  13. UserEx userEx = context.getBean(UserEx.class); // 1.
  14. GameEx gameEx = context.getBean(GameEx.class); // 2.
  15. gameEx.setUserEx(userEx); // 3.
  16. gameEx.printUserEx(); // 4.
  17. }
  18. }

进行执行后,我们可以从控制台中观察到相关输出:

  1. UserEx 无参构造函数
  2. // 其他log信息
  3. 调用setUserEx
  4. com.compilemind.guide.chapter00.spring.UserEx@5300f14a

在上面的例子中,我们手动进行了依赖的管理,那么Spring的IOC容器是否可以帮助我们去管理依赖吗?答案是肯定的。我们只需要在需要注入依赖字段的setter方法上(后面会介绍其他的方式),加上@Autowired注解即可实现这样的功能:

  1. @Component
  2. public class GameEx {
  3. private UserEx userEx;
  4. @Autowired // 使用注解 @Autowired,表明希望IOC容器为我们注入这个UserEx的实例
  5. public void setUserEx(UserEx userEx) {
  6. this.userEx = userEx;
  7. System.out.println("调用setUserEx");
  8. }
  9. // 忽略其他代码
  10. }

此时,我们不再需要分别从IOC容器中获取UserEx和GameEx来手动设置依赖,因为SpringIOC容器已经帮助我们完成了:

  1. @SpringBootApplication //
  2. public class IocApp {
  3. public static void main(String[] args) {
  4. // 1. 启动
  5. ConfigurableApplicationContext context =
  6. SpringApplication.run(IocApp.class, args);
  7. dependencyInject(context);
  8. }
  9. /**
  10. * 不再需要手工设置了
  11. */
  12. private static void dependencyInject(ConfigurableApplicationContext context) {
  13. GameEx gameEx = context.getBean(GameEx.class);
  14. gameEx.printUserEx();
  15. }
  16. }

最后的输出与上面手动设置依赖是相同。

本章总结

在本章中,我们了解了Java中关于反射的一些基础知识,了解了如何通过反射而不是new的形式创建对象。在此基础上,我们介绍了Spring IOC容器,让大家明白了Spring IOC底层的基本原理。最后,我们介绍了IOC控制反转以及DI依赖注入的概念,并用Spring框架演示了这些概念。在下一章,我们将介绍使用SpringIOC容器来创建Bean的几种方式。

极简SpringBoot指南-Chapter00-学习SpringBoot前的基本知识的更多相关文章

  1. 黑科技抢先尝(续2) - Windows terminal中Powershell Tab的极简美化指南

    目录 安装python 安装git 安装powerline字体 主题定制 安装oh-my-posh 查看策略组的执行权限 使用choco 安装终端模拟器 - ConEmu 优化 PowerShell ...

  2. 极简SpringBoot指南-Chapter04-基于SpringBoot的书籍管理Web服务

    仓库地址 w4ngzhen/springboot-simple-guide: This is a project that guides SpringBoot users to get started ...

  3. 极简SpringBoot指南-Chapter03-基于SpringBoot的Web服务

    仓库地址 w4ngzhen/springboot-simple-guide: This is a project that guides SpringBoot users to get started ...

  4. 《Springboot极简教程》问题解决:Springboot启动报错 Whitelabel Error Page: This application has no explicit mapping for(转)

    13.2 Spring Boot启动报错:Whitelabel Error Page 13.2 Spring Boot启动报错:Whitelabel Error Page 问题描述 Whitelabe ...

  5. 黑科技抢先尝(续) - Windows terminal中WSL Linux 终端的极简美化指南

    目录 修改默认源,为apt-get安装提速 安装python 和 python pip 安装 zsh 安装powerline-font中的特定字体 安装powerline-shell 修改~目录下的配 ...

  6. 极简SpringBoot指南-Chapter02-Spring依赖注入的方式

    仓库地址 w4ngzhen/springboot-simple-guide: This is a project that guides SpringBoot users to get started ...

  7. SpringBoot系列: 极简Demo程序和Tomcat war包部署

    =================================SpringBoot 标准项目创建步骤================================= 使用 Spring IDE( ...

  8. SpringBoot与SpringCloud学习指南

    推荐一个Spring Boot的导航网站:Spring Boot 中文导航 Spring boot使用的各种示例,以最简单.最实用为标准 spring-boot-helloWorld:spring-b ...

  9. springboot 极简使用例子: redis,MySql数据库,日志,netty,打包和运行

    配置 创建项目的时候选择 application.yml内容如下 spring: redis: host: 127.0.0.1 port: 6379 database: 0 datasource: d ...

随机推荐

  1. TCP请求连接与断开

    TCP连接的三次握手:

  2. 从拟阵基础到 Shannon 开关游戏

    从拟阵基础到 Shannon 开关游戏 本文中的定理名称翻译都有可能不准确!如果有找到错误的同学一定要联系我! 本文长期征集比较好的例题,如果有比较典型的题可以联系我 目录 从拟阵基础到 Shanno ...

  3. 使用Hugo和GitHub搭建博客

    折腾了几天博客的框架终于搭建起来了.研究了一番之后,最终还是选择使用Hugo和GitHub来搭建博客.本文介绍了如何使用Hugo来搭建静态博客网站,并将其部署在GitHub上.使用https://&l ...

  4. opencv入门系列教学(四)处理鼠标事件

    一.鼠标事件的简单演示 opencv中的鼠标事件,值得是任何与鼠标相关的任何事物,例如左键按下,左键按下,左键双击等.我们先来看看鼠标事件有哪些,在python中执行下面代码: import cv2 ...

  5. 编辑器扩展 --- 自动化处理之AssetPostprocessor资源导入

    AssetPostprocessor资源导入管线 AssetPostprocessor用于在资源导入时自动做一些设置,比如当导入大量图片时,自动设置图片的类型,大小等.AssetPostprocess ...

  6. opencv入门系列教学(六)图像上的算术运算(加法、融合、按位运算)

    0.序言 这一篇博客我们将学习图像的几种算术运算,例如加法,减法,按位运算等. 1.图像加法 我们可以通过OpenCV函数 cv.add() 或仅通过numpy操作 res=img1+img2 res ...

  7. JS 之 每日一题 之 算法 ( 有多少小于当前数字的数字 )

    给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目. 换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i 且 ...

  8. Redis详解(三)——

    redis https://www.cnblogs.com/zhangyinhua/p/14504717.html

  9. kubeadm方式搭建K8S集群

    一.kubeadm介绍 二.安装要求 三.集群规划 四.环境初始化(在每个服务器节点操作) 1.关闭防火墙 2.关闭selinux 3.关闭swap 4.根据规划设置主机名 5.在Master添加ho ...

  10. 案例九:shell脚本自动创建多个新用户,并设置密码

    此脚本是用来批量创建用户并设置用户密码,在企业用非常实用. 脚本一 #!/bin/bash for name in $( seq 1 100 ) do useradd "user$name& ...