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

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

1、工程背景

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

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

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

2、踩坑之路

2.一、开始搭建

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

...
<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应用,结构十分简单(甚至用不着建包,此处为了方便理解),省略个性化部分后,整块目录结构以下:spring

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

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

...

@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 类,用于调用相关方法:apache

...

@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

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

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.三、尝试解决

(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、深刻理解

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 容器中,容许按类型或者名称获取 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、总结反思

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》杨开振

相关文章
相关标签/搜索