通过Spring Boot的非web应用理解全注解下的Spring IoC

通过Spring Boot的非web应用理解全注解下的Spring IoC

一、工程背景

我曾经搭建了一个系统,把这个系统运行在阿里云服务器上。因为我只有一个云服务器可以运行,所以开发的系统是单机运行的。没有额外的地方可以存放静态资源,那么系统里的静态资源——主要是图片,只能存放在这台服务器上。当多用户访问这台服务器的时候,受限于此服务器捉襟见肘的1M宽带,用户们会明显感觉图片加载缓慢,难以忍受长时间等待。其实如果不加载图片的话,这台服务器能顶住足够多的用户同时访问的压力,只是无奈图片资源这些耗流量大户占用大量宽带,我也没有足够资金提高服务。

为了提高图片加载速度,我打算将图片资源存放在另外网络比较好的地方。一般就是考虑云计算提供商的对象存储产品,刚好腾讯云对象存储这款产品价格实惠,所以入手了一个。把我服务器里面的图片迁移出来,腾讯云有提供多端的工具可以使用,这些都简单实用,但是大量图片迁移会产生很多重复工作,这里腾讯云没有合适的工具供我选择,而且我在迁移完成之后要对相应数据表进行更新字段信息的操作,为了满足我个性化的需求,更快更好完成图片迁移,我决定自己开发一个简单的工具来迁移图片。

简单规划了一下,我决定把开发迁移工具采用 Spring Boot 框架来搭建,做出一个非 web 应用。

二、踩坑之路

2.1、开始搭建

快速建立 Spring Boot 项目,项目的依赖有(省略其他不紧要的内容)

...
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->    
</parent>
...
<properties>
	<java.version>1.8</java.version>
</properties>
...
<dependencies>
    <!-- Spring starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- Spring JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- 自动插入工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- 腾讯云对象存储SDK -->
    <dependency>
        <groupId>com.qcloud</groupId>
        <artifactId>cos_api</artifactId>
        <version>5.6.24</version>
    </dependency>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

沿用以往的构建思路,并且去掉控制层,搭建非web应用,结构十分简单(甚至用不着建包,此处为了方便理解),省略个性化部分后,整块目录结构如下:

...
│
└── src
    ├── test 使用Junit对java中的源代码进行测试
    └── main 主要文件目录
        ├── resources 资源根目录(包含项目配置文件)
        └── java 源代码(包含主程序)
            └──... 自建包目录(比如com.xxx.xxx)   
                ├── entity  实体类目录
                ├── repository  数据访问层目录
                ├── service  服务层目录
                └── ***Application.java springboot启动类

首先创建 spring boot 启动类,很常见很简单,类上添加 @SpringBootApplication 注解:

...

@SpringBootApplication
public class ImageMigrationApplication {
	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
	}
}

然后写实体类(省略部分字段),类上添加 @Entity 注解(@Data@Builder@AllArgsConstructor@NoArgsConstructor 是 lombok 的注解,用于简化开发),继续写 Spring 的 JPA 里的 repository 接口(省略无关方法),用于数据访问,类上添加 @Repository 注解:

...

@Entity(name = "sys_survey_image")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TbSurveyImageEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, updatable = false, length = 11)
    private Long id;

    /**
     * 图片类型
     */
    @Column(nullable = false)
    private String type;

    /**
     * 图片名称
     */
    private String name;

    /**
     * 相对路径
     */
    @Column(nullable = false)
    private String virtualPath;

    /**
     * 本地路径
     */
    @Column(nullable = false)
    private String diskPath;

    /**
     * 腾讯云对象存储路径,新添字段
     */
    private String qcloudPath;

    ...

}

...

@Repository
public interface MigrationRepository extends JpaRepository<TbSurveyImageEntity, Long> {
	...
}

接着是服务提供类,类上添加 @Service 注解( @Slf4j 不是spring的注解,是 lombok 的注解,这里为了方便开发),参考腾讯云提供的文档(点击查看),使用 @Autowired 注解注入刚才写好的依赖的 MigrationRepository 类,用于调用相关方法:

...

@Slf4j
@Service
public class MigrationService {

    @Autowired
    private MigrationRepository migrationRepository;

    // 1 初始化用户身份信息(secretId, secretKey)
    String secretId = "COS_SECRETID";
    String secretKey = "COS_SECRETKEY";
    COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
    // 2 设置 bucket 的区域
    // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法
    Region region = new Region("COS_REGION");
    ClientConfig clientConfig = new ClientConfig(region);
    // 3 生成 cos 客户端
    COSClient cosClient = new COSClient(cred, clientConfig);

    /**
     * 查询存储桶列表
     * @return List<Bucket> 桶列表
     */
    public List<Bucket> getBucketList () {
        List<Bucket> buckets = cosClient.listBuckets();
        for (Bucket bucketElement : buckets) {
            String bucketName = bucketElement.getName();
            String bucketLocation = bucketElement.getLocation();
            log.info("bucketName:" + bucketName + " bucketLocation:" + bucketLocation);
        }
        return buckets;
    }

    /**
     * 获取本地图片列表,一次最多1000条数据
     * @return 返回图片列表
     */
    public List<TbSurveyImageEntity> getImgageList () {
        Pageable pageable = PageRequest.of(0, 1000);
        Page<TbSurveyImageEntity> imageEntityPage = migrationRepository.findAll(pageable);
        log.info("Total number of images:" + imageEntityPage.getTotalElements());
        return imageEntityPage.getContent();
    }

	...

}

最后把配置信息补充完整,用于启动并运行。相关配置文件在资源目录 resources 目录下的 application.yml 下,因为本地开发和阿里云服务器的运行环境不一样,所以会有 application-dev.ymlapplication-prd.yml 等类似 application-*.yml 文件名的配置文件,用于区分不同环境下的配置信息。这里展示本地运行环境下配置信息,阿里云的几乎一样(yml文件的配置与properties文件只是简写和缩进的差别,差异不大,我习惯采用yml文件)

spring:
  # 数据访问配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/[数据库名字]?serverTimezone=GMT%2B8&useSSL=false
    username: [数据库账号]
    password: [数据库密码]
  jpa:
    database: mysql
    hibernate:
      ddl-auto: update
    show-sql: true

至此,该工具的工程初步建立,并且可以开始测试应用是否可以运行,测试与腾讯云、本地数据库和本地文件是否可以访问。

2.2、出现问题
(1) 启动和访问腾讯云成功

开始测试是否能启动和腾讯云对象存储服务连通性,在启动类中添加服务类,如下

@SpringBootApplication
public class ImageMigrationApplication {
	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
		MigrationService migrationService = new MigrationService();
		migrationService.getBucketList();
	}
}

测试结果:成功运行,并且打印出存储桶列表

【】:该符号标注日志关键信息行

...
2020-07-12 16:52:16.117  INFO 17100 --- [           main] c.d.i.m.ImageMigrationApplication    : The following profiles are active: dev
2020-07-12 16:52:16.809  INFO 17100 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2020-07-12 16:52:16.907  INFO 17100 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 85ms. Found 1 JPA repository interfaces.
2020-07-12 16:52:17.562  INFO 17100 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-07-12 16:52:17.673  INFO 17100 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.4.10.Final}
2020-07-12 16:52:17.916  INFO 17100 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-07-12 16:52:18.721  INFO 17100 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-07-12 16:52:18.941  INFO 17100 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-07-12 16:52:18.967  INFO 17100 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
2020-07-12 16:52:19.879  INFO 17100 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-07-12 16:52:19.887  INFO 17100 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
【2020-07-12 16:52:21.298  INFO 17100 --- [           main] c.d.i.m.ImageMigrationApplication    : Started ImageMigrationApplication in 5.766 seconds (JVM running for 6.471)】
【2020-07-12 16:52:21.933  INFO 17100 --- [           main] c.d.i.m.service.MigrationService     : bucketName:image-1258993064 bucketLocation:ap-guangzhou】
2020-07-12 16:52:21.937  INFO 17100 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2020-07-12 16:52:21.941  INFO 17100 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2020-07-12 16:52:21.952  INFO 17100 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

Process finished with exit code 0
(2) 访问本地数据库失败

接着继续测试本地数据库是否能够能够访问,这里就是测试是否能够获取图片表信息,在启动类中添加调用方法,如下:

@SpringBootApplication
public class ImageMigrationApplication {
	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
		MigrationService migrationService = new MigrationService();
		migrationService.getBucketList();
		migrationService.getImgageList();
	}
}

出现 NPE 异常信息:

...
2020-07-12 17:05:15.862  INFO 4816 --- [           main] c.d.i.m.ImageMigrationApplication    : Started ImageMigrationApplication in 5.266 seconds (JVM running for 6.076)
2020-07-12 17:05:16.862  INFO 4816 --- [           main] c.d.i.m.service.MigrationService     : bucketName:image-1258993064 bucketLocation:ap-guangzhou
2020-07-12 17:05:16.867  INFO 4816 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2020-07-12 17:05:16.870  INFO 4816 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...

【Exception in thread "main" java.lang.NullPointerException
	at cn.dxystudy.image.migration.service.MigrationService.getImgageList(MigrationService.java:67)
	at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:20)】
	
2020-07-12 17:05:16.897  INFO 4816 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

Process finished with exit code 1

在 MigrationService 类中再写了几个调用 MigrationRepository 接口的其他方法,重新测试,出现同样的错误。

所以该错误定位在 Service 获取不到 MigrationRepository 类的实例。

2.3、尝试解决
(1)在启动类中注入 MigrationService
@SpringBootApplication
public class ImageMigrationApplication {

	@Autowired
	MigrationService migrationService;

	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
		migrationService.getBucketList();
		migrationService.getImgageList();
	}
}

结果:编译不通过(Java基础知识,属低级错误)

Non-static field 'migrationService' cannot be referenced from a static context
不能从静态上下文中引用非静态字段“migrationService”
(2)在启动类中注入 静态MigrationService
@SpringBootApplication
public class ImageMigrationApplication {

	@Autowired
	static MigrationService migrationService;

	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
		migrationService.getBucketList();
		migrationService.getImgageList();
	}
}

结果:编译通过,运行异常,抛出NPE

Exception in thread "main" java.lang.NullPointerException
	at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:21)
(3)取消注入 MigrationRepository ,通过 new 创建实例。
private MigrationRepository migrationRepository = new MigrationRepository();

结果:编译不通过(Java基础知识,属低级错误)

'MigrationRepository' is abstract; cannot be instantiated
“ MigrationRepository”是抽象的;无法实例化
(4)尝试将 MigrationRepository 懒加载设置为false
@Repository
@Lazy(false)
public interface MigrationRepository extends JpaRepository<TbSurveyImageEntity, Long> {
    ...
}

结果:NPE

Exception in thread "main" java.lang.NullPointerException
	at cn.dxystudy.image.migration.service.MigrationService.getImgageList(MigrationService.java:67)
	at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:21)
(5)实现 CommandLineRunner 类并且重写 run 方法

参考官方文档:Spring Boot 2.2.4.RELEASE Reference

@SpringBootApplication
public class ImageMigrationApplication implements CommandLineRunner {

	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
	    MigrationService migrationService = new MigrationService();
		migrationService.getBucketList();
		migrationService.getImgageList();
	}
}

结果:运行异常,因为 NPE ,仍然找不到 MigrationRepository 实例

java.lang.IllegalStateException: Failed to execute CommandLineRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:787) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:768) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:322) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE]
	at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:21) [classes/:na]
Caused by: java.lang.NullPointerException: null
	at cn.dxystudy.image.migration.service.MigrationService.getImgageList(MigrationService.java:67) ~[classes/:na]
	at cn.dxystudy.image.migration.ImageMigrationApplication.run(ImageMigrationApplication.java:28) [classes/:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:784) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE]
	... 5 common frames omitted
(6)实现 CommandLineRunner 类并且重写 run 方法,注入 MigrationService 依赖
@SpringBootApplication
public class ImageMigrationApplication implements CommandLineRunner {

    @Autowired
	MigrationTestService migrationTestService;

	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		migrationService.getBucketList();
		migrationService.getImgageList();
	}
}

结果:运行成功,并打印出腾讯云桶列表和数据库中图片总数

...
2020-07-12 17:35:55.316  INFO 24544 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-07-12 17:35:56.771  INFO 24544 --- [           main] c.d.i.m.ImageMigrationTestApplication    : Started ImageMigrationTestApplication in 5.434 seconds (JVM running for 6.159)
2020-07-12 17:35:57.502  INFO 24544 --- [           main] 【c.d.i.m.service.MigrationTestService     : bucketName:image-1258993064 bucketLocation:ap-guangzhou】
Hibernate: select ...
【2020-07-12 17:35:57.791  INFO 24544 --- [           main] c.d.i.m.service.MigrationTestService     : Total number of images:64】
2020-07-12 17:35:57.796  INFO 24544 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
...

继续测试其他方法,均运行成功。

三、深入理解

3.1 Spring 的控制反转(IoC)的应用

Spring 依赖两个核心理念,一个是控制反转(Inversion of Control,IoC),另一个是面向切面编程(Aspect Oriented Programming,AOP)IoC 容器是 Spring 的核心,可以说 Spring 是一种基于 IoC 容器编程的框架。Spring Boot 是基于注解开发的 Spring IoC。

IoC 是一种通过描述来生成或者获取对象的技术,这个技术不是 Spring 甚至不是 Java 独有的。对于 Java 初学者更多的时候所熟悉的是使用 new 关键字来创建对象,而在 Spring 中则不是,它是通过描述来创建对象。Spring Boot 建议使用注解的描述(XML也可以)来生成对象。

一个系统可以生成各种对象,并且这些对象都需要进行管理。另外,对象之间并不是孤立的,它们之间还可能存在依赖关系。为此,Spring 还提供了依赖注入的功能,使得我们能够通过描述来管理各个对象之间的关系。

例如,一个班级是由多个老师和同学组成的,那么班级就依赖于多个老师和同学了。

为描述上述的班级、同学和老师这3个对象关系,这里需要一个容器。在Spring中把每一个需要的管理的对象成为Spring Bean(简称Bean),而 Spring 管理这些 Bean 的容器,被称为 Spring IoC容器(简称 IoC容器)。

IoC 容器需要具备两个基本功能:

  • 通过描述管理 Bean ,包括发布和获取 Bean
  • 通过描述完成 Bean 之间的依赖关系
3.2 IoC 容器简介

Spring IoC 容器是一个管理 Bean 的容器,在 Spring 的定义中,它要求所有的 IoC 容器都需要实现接口 BeanFactory(它是一个顶级容器接口)。

源码理解:参考博文Spring IOC 容器源码分析

在 Spring IoC 容器中,允许按类型或者名称获取 Bean ,这对理解 Spring 的 依赖注入(Dependency Injection,DI) 是十分重要的。

默认情况下,Bean 都是以单例存在的,也就是使用 getBean 方法返回的都是同一个对象。

由于 BeanFactory 的功能还不够强大,因此 Spring 在 BeanFactory 的基础上,还设计一个更为高级的接口 ApplicationContext。它是 BeanFactory 的子接口之一,在 Spring 的体系中 BeanFactoryApplicationContext 是最为重要的接口设计,在现实中使用的大部分 Spring IoC 容器是 ApplicationContext 接口的实现类。ApplicationContext 接口通过继承上级接口,进而继承 BeanFactory 接口,但是在 BeanFactory 的基础上,扩展了消息国际化接口(MessageSource)、环境可配置接口(EnvironmentCapable)、应用事件发布接口(ApplicationEventPublisher)和资源模式(ResourcePatternResolver)接口,所以它 功能会更为强大。

[Spring IoC容器的接口设计:略]

在 Spring Boot 当中主要通过注解来装配 Bean 到 Spring IoC 容器中,相关的是 AnnotationConfigApplicationContext ,它是基于注解的 IoC 容器。

AnnotationConfigApplicationContext 会用到两个注解(@Configuration@Bean)来定义 Java 配置文件和Bean,并将 Java 配置信息传递给它的构造方法,这样它就可以读取配置了,然后将配置里的 Bean 装配到 IoC 容器中。

举个例子:

在同个包目录下创建以下三个文件。

首先定义一个 Java 简单对象(Plan Ordinary Java Object, POJO)

import lombok.Data;

@Data
public class User {
    private Long id;
    private String userName;
    private String note;
    
    // @Data: getter & setter
}

然后定义一个Java配置文件 AppConfig.java

// @Configuration 代表这是一个Java配置文件,Spring容器会根据它来生成IoC容器去装配Bean
@Configuration
public class AppConfig {
    // @Bean 代表将 initUser 方法返回的POJO装配到IoC容器中
    @Bean(name = "user") // 属性name定义这个Bean的名称,如果没有则将方法名称作为Bean的名称
    public User initUser () {
        User user = new User();
        user.setId(10010L);
        user.setUserName("user_name_1");
        user.setNote("note_1");
        return user;
    }
}

使用 AnnotationConfigApplicationContext 来构建自己的 IoC容器

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.logging.Logger;

public class IoCTest {
    private static Logger log = Logger.getLogger(String.valueOf(IoCTest.class));
    public static void main(String[] args) {
        // 传入java配置文件到构造方法中,读取配置,然后装配Bean到IoC容器中
		ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // 于是可以使用getBean方法获取对应的POJO
        User user = ctx.getBean(User.class);
        log.info(String.valueOf(user.getId()));
    }
}

运行结果:最后打印出了预期结果 10010

19:49:55.415 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@161cd475
19:49:55.456 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
19:49:55.667 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
19:49:55.671 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
19:49:55.673 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
19:49:55.678 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
19:49:55.689 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalPersistenceAnnotationProcessor'
19:49:55.696 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
19:49:55.706 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user'
【七月 12, 2020 7:49:55 下午 cn.dxystudy.image.migration.spring.ioc.IoCTest main
信息: 10010】
3.3 装配Bean

如果一个个的 Bean 使用注解 @Bean 注入 Spring IoC 容器中,那将是一件很麻烦的事情。好在 Spring 还允许进行扫描装配 Bean 到 IoC 容器中,对于扫描装配而言使用的注解是 @Component@ComponentScan

  • @Component:标明哪个类被扫描进入Spring IoC容器

  • @ComponentScan:标明采用何种策略去扫描装配Bean

在 Spring 中,@Service@Repository 注解都注入了 @Component ,所以在默认情况下它会被 Spring 扫描装配到 IoC 容器中。

[源码理解:略]

举个例子:

将上个例子的 User.java 移入到 config 包内(代码省略包信息),然后进行修改:

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
// 如果不配置name,那么IoC容器就会把第一个字母作为小写,其他不变作为Bean名称放入IoC容器中
@Component("user") 
public class User {
   // @Value指定具体的值,使得Spring IoC给予对应的属性注入对应的值
   @Value("1001")
   private Long id;
   @Value("user_name_1")
   private String userName;
   @Value("note_1")
   private String note;
   
   // @Data: getter & setter
}

为了让 Spring IoC 容器装配这个类,需要改造 AppConfig 类

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

// @Configuration 代表这是一个Java配置文件,Spring容器会根据它来生成IoC容器去装配Bean
@Configuration
// 新增@ComponentScan注解,意味着它会进行扫描,但是它智慧扫描该类所在的当前包和其子包
@ComponentScan
public class AppConfig {
   // 删除之前使用@Bean标注的创建对象方法
}

在原来的测试类中,重新导入 User 类,然后进行测试,测试结果:最后打印出了预期结果1001

20:37:52.162 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@161cd475
20:37:52.191 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
20:37:52.302 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\Users\22920\IdeaProjects\imagemigration\target\classes\cn\dxystudy\image\migration\spring\ioc\config\User.class]
20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
20:37:52.422 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalPersistenceAnnotationProcessor'
20:37:52.422 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
20:37:52.438 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user'
【七月 12, 2020 8:37:52 下午 cn.dxystudy.image.migration.spring.ioc.IoCTest main
信息: 1001】

为了使得 User 类能够被扫描,我把它迁移到了本不改放置它的配置包,这样显然就不太合理。为了更加合理,@Component 还允许自定义扫描的包,这里不再讨论。

现实的 Java 应用往往需要引入许多第三方的包,并且很有可能希望把第三方的包的类对象也放入到 Spring IoC 容器中,这时可以使用 @Bean 注解。

举个例子:
引入 DBCP 数据源,在 pom.xml 文件上加入项目所需要的 DBCP 包和数据库 MySQL 驱动程序的依赖

<dependency>
	<groupId>org.apache.commons</groupId>
   <artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
	<groupId>mysql</groupId>
   <artifactId>mysql-connetor-java</artifactId>
</dependency>

接着使用它提供的机制来生成数据源。修改上个例子的 AppConfig.java 文件,增加 getDataSource 方法

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AppConfig {
   @Bean(name = "dataSorce")
   public DataSource getDataSorce () {
       Properties props = new Properties();
       props.setProperty("dirver", "com.mysql.cj.jdbc.Driver");
       props.setProperty("url", "jdbc:mysql://localhost:3306/[数据库名字]?serverTimezone=GMT%2B8&useSSL=false");
       props.setProperty("username", "[数据库用户名]");
       props.setProperty("password", "[数据库用户密码]");
       DataSource dataSource = null;
       try {
           dataSource = BasicDataSourceFactory.createDataSource(props);
       } catch (Exception e) {
           e.printStackTrace();
       }
       return dataSource;
   }
}

启动测试类,结果:打印出创建 Bean 的信息

...
21:10:56.588 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
21:10:56.599 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user'
21:10:56.636 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dataSorce'
...
3.4 依赖注入

以上讨论了如何将 Bean 装配到 IoC 容器中和如何获取 Bean ,这节讨论 Bean 之间的依赖。

在 Spring IoC 的概念中,称之为依赖注入(Dependency Injection,DI)。

例如:

人类(Person)有时候利用一些动物(Animal)去完成一些事情,比如说狗(Dog)是用来看门的,猫(Cat)是用来抓老鼠的,鹦鹉(Parrot)是用来迎客的……于是做一些事情就依赖于那些可爱的动物了。

定义人类和动物接口

/**
* 人类接口
*/
public interface Person {

   // 使用动物来做些事情
   public void use();

   // 设置动物
   public void setAnimal(Animal animal);

}
/**
* 动物接口
*/
public interface Animal {
   // 动物可以做点事情
   public void work();
}

定义两个实现类,都需要标注 @Component

import org.springframework.beans.factory.annotation.Autowired;

/**
* 普通人
*/
@Component // 加入注解标明将装配进Spring IoC容器
public class OrdinaryPeople implements Person{
   
   // 这里的Dog类是动物的一种,所以Spring IoC容器会把Dog的实例注入到OrdinaryPeople
   @Autowired 
   private Animal animal;

   @Override
   public void use() {
       this.animal.work();
   }

   @Override
   public void setAnimal(Animal animal) {
       this.animal = animal;
   }
}
import org.springframework.stereotype.Component;

/**
* 狗
*/
@Component // 加入注解标明将装配进Spring IoC容器
public class Dog implements Animal{
   @Override
   public void work() {
       System.out.println("狗【" + Dog.class.getSimpleName() + "】是用来看门的");
   }
}

创建配置类和场景类

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class DIAppConfig { }
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
* 在家里有个普通人在用动物做些事情
*/
public class Home {

   public static void main(String[] args) {
       ApplicationContext ctx = new AnnotationConfigApplicationContext(DIAppConfig.class);
       OrdinaryPeople ordinaryPeople = ctx.getBean(OrdinaryPeople.class);
       ordinaryPeople.use();
   }
   
}

运行结果:运行成功并且打印出“狗【Dog】是用来看门的”。

说明通过注解 @Autowired 成功地将 Dog 注入到了 OrdinaryPeople 实例中。

...
21:56:37.052 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'DIAppConfig'
21:56:37.061 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog'
21:56:37.061 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ordinaryPeople'
狗【Dog】是用来看门的

还要要注意的是 @Autowired 是一个默认必须找到对应 Bean 的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为null,那么就可以配置 @Autowired 属性requiredfalse

@Autowired(required = false) // 设置为非必须Bean

除了标注属性外,还可以标注方法

@Override
@Autowired // 标注方法
public void setAnimal(Animal animal) {
    this.cat = animal;
}

上面的例子,只是创建了一个动物——狗,而实际上还可以有猫(Cat),如果又创建一个猫,

/**
* 猫
*/
@Component // 加入注解标明将装配进Spring IoC容器
public class Cat implements Animal{
   @Override
   public void work() {
       System.out.println("猫【" + Dog.class.getSimpleName() + "】是用来抓老鼠的");
   }
}

则会在场景类中运行出错抛出异常,因为 Spring IoC 不知道注入哪个动物。产生注入失败的根本原因是按类型(by type)查找,这样问题成为歧义性。

...
【Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.dxystudy.image.migration.spring.di.Animal' available: expected single matching bean but found 2: cat,dog】
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1265)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1207)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)
	... 14 more

如果需要狗看门,那么可以把属性名称转化为 dog ,注入修改为

@Autowired
private Animal dog; // 其他地方引用的同样修改成dog

运行结果:运行成功并且打印出“狗【Dog】是用来看门的”

同样的如果需要猫抓老鼠,那么可以把属性名称转化为cat,同样也是运行成功并且打印出“猫【Dog】是用来抓老鼠的”

为了使 @Autowired 能够继续使用,将 OrdinaryPeople 的属性名称从 animal 修改成 dog 或者 cat。显然这是一个憋屈的做法,好好一个动物却被我们定义成了狗/猫。消除歧义性还能利用 @Primary@Quelifier 两个注解,这两个注解从不同角度去解决歧义性问题。

  • @Primary:修改优先权的注解(问题依然存在:当多个类型同时存在该注解时)
  • @Quelifier:与@Autowired组合在一起,通过类型和名称一起找到Bean

具体不再讨论。

默认的情况是不带参数构造方法下实现依赖注入。但事实上,有些类只有带有参数的构造方法。为了满足这个功能,可以使用 @Autowired 注解对构造方法参数进行注入。

举个例子:

修改 OrdinaryPeople 类来满足这个功能。

import ...
/**
 * 普通人
 */
@Component // 加入注解标明将装配进Spring IoC容器
public class OrdinaryPeople implements Person{

    private Animal animal;

    // @Autowired @Qualifier两个组合注解为了消除歧义性
    public OrdinaryPeople (@Autowired @Qualifier("dog") Animal animal) {
        this.animal = animal;
    }

    @Override
    public void use() {
        this.animal.work();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

}

运行结果:运行成功并且打印出狗的预期结果

3.5 生命周期

以上只是关心如何正确地将 Bean 装配到 IoC 容器中,而没有关心 IoC 容器如何装配和销毁 Bean 的过程。

有时候也需要自定义初始化或者销毁 Bean 的过程,以满足一些 Bean 的特殊初始化和销毁的要求。

例如,在上面使用数据源的例子中,我们希望在其关闭的时候调用 close 方法,以释放数据库的链接资源,这是在项目使用过程中很常见的要求。

这节来了解 Spring IoC 初始化和销毁 Bean 的过程,也就是 Bean 的声明周期的过程,它大致分为 Bean定义Bean的初始化Bean的生存期Bean的销毁 4个部分。

其中Bean定义过程大致如下:

  1. Spring 通过配置(如 @ComponentScan 定义的扫描路径)去找到带有 @Component 的类。【资源定位】
  2. 找到资源后,开始解析,并且将定义的信息保存起来。【Bean定义】(注意此时没有初始化Bean即没有Bean的实例,仅仅定义)
  3. 把 Bean 定义发布到 Spring IoC 容器中。【发布Bean定义】(还是没有 Bean 的实例,仅仅发布定义)

这三步只是资源定位并将 Bean 的定义发布到 IoC 容器的过程,还没有 Bean 实例的生成,更没有完成依赖注入。

在默认情况下,Spring 会继续去完成 Bean 的实例化和依赖注入,这样从 IoC 容器中就可以得到一个依赖注入完成的 Bean。但是有些 Bean 会受到变化的因素影响,这是倒希望是取出 Bean 的时候完成呢个初始化和依赖注入,也就是说让那些 Bean 只是将定义发布到 IoC 容器中而不做实例化和依赖注入,当要取出来的时候才做初始化和依赖注入等操作。

Spring 初始化 Bean :

资源定位 —> Bean定义 —> 发布Bean定义 —> 实例化 —> 依赖注入—> ……

@ComponentScan 中还有一个配置项 lazyInit ,只可以配置 Boolean 值,且默认为 false ,也就是默认不进行延迟初始化,因此在默认的情况下 Spring 会对 Bean 进行实例化和依赖注入对应的属性值。

举个例子:

改造上个例子的 OrdinaryPeople

import ...

/**
* 普通人
*/
@Component
public class OrdinaryPeople implements Person{

   private Animal animal;


   @Override
   public void use() {
       this.animal.work();
   }

   @Override
   @Autowired
   @Qualifier("dog")
   public void setAnimal(Animal animal) {
       System.out.println("依赖注入");
       this.animal = animal;
   }
   
}

在场景类 Home 中对 OrdinaryPeople ordinaryPeople = ctx.getBean(OrdinaryPeople.class); 这行打上断点进行调试。

运行结果:运行成功,并且运行到断点之前打印出“依赖注入”。

...
23:25:43.849 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cat'
23:25:43.849 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog'
23:25:43.850 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ordinaryPeople'
依赖注入
狗【Dog】是用来看门的

再在配置类 DIAppConfig 的 @ComponentScan 中加入 lazyInit 配置,如下

@ComponentScan(lazyInit = true)

运行结果:运行成功,并且运行到断点之后打印出“依赖注入”。这是因为把它修改为了延迟初始化, Spring 并不会在发布Bean定义后马上完成实例化和依赖注入。

如果仅仅是实例化和依赖注入还是比较简单的,还不能完成进行自定义的要求。为了完成依赖注入的功能,Spring 在完成依赖注入之后,还进行一系列的流程来完成它的生命周期。

Spring Bean 的生命周期:

· ——> 初始化 ——> 依赖注入 ——> setBeanName方法 ——> setBeanFactory方法 ——> setApplicationContext方法 ——> postProcessBeforeInitialization方法 ——> 自定义初始化方法 ——> afterPropertiesSet方法 ——> postProcessAfterInitialization犯法——> <生存期> ——> 自定义销毁方法 ——> destroy方法——> ·

条件装配 Bean 和 Bean 的作用域:略

四、总结反思

4.1 优化代码

将腾讯云存储相关配置信息提取到配置文件 application.yml 中,并使用 @Value 注解对属性赋值。

# 项目配置
migration:
  # 腾讯云
  qcloud:
    secretId: AKIDlIacP****G1WtHSOtGbg
    secretKey: YrvjGd7****bOyM9hmbBVx
    region-name: ap-guangzhou

# 框架配置
spring:
  # 数据访问配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/image***?serverTimezone=GMT%2B8&useSSL=false
    username: r**t
    password: ****
  jpa:
    database: mysql
    hibernate:
      ddl-auto: update
    show-sql: true
  # 彩色日志输出
  output:
    ansi:
      enabled: always

服务提供类 MigrationService.java 中将改造获取腾讯云对象存储客户端的方式,除了再写自己的业务方法,再增加关闭连接的方法。

@Slf4j
@Service
public class MigrationService {

    @Autowired
    private MigrationRepository migrationRepository;

    /**
     * 腾讯云对象存储客户端
     */
    private COSClient cosClient;

    @Autowired
    public void getConnection (@Value("${migration.qcloud.secretKey}") String secretKey
            ,@Value("${migration.qcloud.secretId}") String secretId
            ,@Value("${migration.qcloud.region-name}") String regionName) {
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
        Region region = new Region(regionName);
        ClientConfig clientConfig = new ClientConfig(region);
        cosClient = new COSClient(cred, clientConfig);
        log.info("Initialized COSClient");
    }

    /**
     * 查询存储桶列表
     * @return List<Bucket> 桶列表
     */
    public List<Bucket> getBucketList () {
        List<Bucket> buckets = cosClient.listBuckets();
        for (Bucket bucketElement : buckets) {
            String bucketName = bucketElement.getName();
            String bucketLocation = bucketElement.getLocation();
            log.info("bucketName:" + bucketName + " bucketLocation:" + bucketLocation);
        }
        return buckets;
    }

    /**
     * 从腾讯云对象存储中获取对象列表
     */
    public void getObjectListFromQcloud () {
		...
    }

    /**
     * 获取本地图片列表,一次最多1000条数据
     * @return 返回图片列表
     */
    public List<TbSurveyImageEntity> getImgageList () {
        Pageable pageable = PageRequest.of(0, 1000);
        Page<TbSurveyImageEntity> imageEntityPage = migrationRepository.findAll(pageable);
        log.info("Total number of images:" + imageEntityPage.getTotalElements());
        return imageEntityPage.getContent();
    }

    /**
     * 将本地图片上传到腾讯云对象存储
     */
    public void upload2Qcloud () {
        ...
    }
    
    ...

    /**
     * 更新本地图片信息,增加新的腾讯云地址
     */
    private void updataImagePath (TbSurveyImageEntity imageEntity) {
        migrationRepository.save(imageEntity);
    }

    /**
     * 关闭连接
     */
    public void shutdown () {
        // 关闭客户端(关闭后台线程)
        log.info("Shuting down COSClient");
        cosClient.shutdown();
    }
}

启动类实现 CommandLineRunner 类,注入服务依赖后并重写run方法,在run里面调用服务方法。

@SpringBootApplication
public class ImageMigrationApplication implements CommandLineRunner {
    
	@Autowired
	MigrationService migrationService;

	public static void main(String[] args) throws Exception {
		SpringApplication.run(ImageMigrationApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		migrationService.getBucketList();
		migrationService.getObjectListFromQcloud();
		migrationService.getImgageList();
		migrationService.upload2Qcloud();
        ...
		migrationService.shutdown();
	}
    
}

测试运行,运行成功,返回预期效果。

4.2 总结

通过本次 Spring Boot 非web应用的工具开发,遇到了 Spring Bean 获取不到的问题。刚开始使用 new 获取对象实例,但是 java 的实例不归 Spring Ioc 容器管理,Spring Ioc 容器里面没有实例的信息,而我又调用实例的方法,因此出现空指针异常。通过查阅文档,如果要在启动类中运行命令行,需要实现CommandLineRunner类,并注入MigrationService再重写run方法来调用服务方法。注入依赖的时候不能是静态 (static),因为当类加载器加载静态变量时,Spring 上下文尚未加载,所以类加载器不会在bean中正确注入静态类,并且会失败。优化注入腾讯云客户端的代码,通过@Autowired标注方法获取客户端实例。

解决问题的过程关键就是在启动类中注入 MigrationService 类,因为 MigrationService 也注入 MigrationRepository 类,@Autowired 有 Spring 描述 Bean 之间关系的作用,通过 new 获取MigrationService 实例,这个实例不能获取 Spring IoC 管理的东西,不知道 Spring IoC 注入 MigrationRepository 的实例信息,那么就会抛出空指针异常。

整个学习和实践的过程让我深入理解了 Spring IoC 。

参考文档

[1] 腾讯云. 文档中心 > 对象存储 > SDK 文档 > Java SDK > 快速入门

[2] Spring Boot Reference Documentation(2.2.4.RELEASE)

[3] 《深入浅出Spring Boot 2.x》杨开振
[4] Spring IOC 容器源码分析. https://javadoop.com

[5] Mingqi. Spring IoC有什么好处呢?知乎.
[6] JavaGuide. Github.

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值