Spring 依赖注入(注解、Bean、Resource)

Annotation(注解)

@Service
public class MessageServiceImpl implements MessageService{

    public String getMessage() {
         return "Hello World!";
    }

}

观察上面代码,发现多出了一个语法@service

  • 注解是Java推出的一种注释机制,与普通的注释不同,Annotation可以在编译、运行阶段读取的。我们使用他来完成一些增强功能,在Spring中就重度使用了Annotation。
  • 从另一个角度看,Annotation也是一个Java类,只是过于特殊
    image.png

    掌握这5个小点,就能大致理解Annotation的工作机制了。

1. Target

java.lang.annotation.Target自身也是一个注解,它只有一个数组属性,用于设定该注解的目标范围,比如说可以作用于类或方法等。因为是数组,所以可以同时设定多个范围。

具体可以作用的类型配置在java.lang.annotation.ElementType枚举类中,几个常用的

  • ElementType.TYPE
    可以作用于 类、接口类、枚举类上
  • ElementType.FIFLD
    可以作用于 类的属性
  • ElementType.METHOD
    可以作用于 类的方法
  • ElementType.PARAMETER
    可以作用于 类的参数

如果想同时作用于类和方法上,那么可以直接@Target({ElementType.TYPE,ElementType.METHOD})

2. Retention

java.lang.annotation.Retention自身也是一个注解,它用于声明该注解的生命周期,简单来说就是在Java编译、运行的哪个环节有效,它的值定义在java.lang.annotation.RetentionPolicy枚举类中,有三个值可以选择

  • SOURCE:纯注释
  • CLASS:编译阶段有效
  • RUNTIME:运行时有效
    一般来说,自己定义的Annotation设置成运行时有效

3. Document

java.lang.annotation.Documented自身也是一个注解,它的作用是将注解中的元素包含到JavaDoc文档中,一般情况下,都会添加这个注解的。

4. @interface

@interface就是声明当前的Java类型是Annotation,固定语法

5. Annotation 属性

String value() default "";
Annotation的属性有点像类的属性一样,它约定了属性的类型(这个类型是基础类型:String、boolean、int、long)和属性名称(默认名称是value,在引用的时候可以省略),default代表的是默认值。

  • Annotation 属性是可以有多个的,比如下面的一个注解类
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {

    @AliasFor("name")
    String value() default "";


    @AliasFor("value")
    String name() default "";


    boolean required() default true;

    String defaultValue() default ValueConstants.DEFAULT_NONE;
}

@AliasFor("name")是别名的意思

Spring Bean

IoC(Inversion of Control,控制反转)容器最最核心的组件,没有IoC容器就没有Spring框架

  • IoC(Inversion of Control,控制反转)是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度
    在Spring框架中,主要通过依赖注入来实现IoC

在Spring中,所有的Java对象都会通过IoC容器转变为Bean(Spring对象的一种称呼),构成应用程序主干和由Spring IoC容器管理的对象称为beans,beans和他们之间的依赖关系反映在容器使用的配置元数据中。基本上所有的Bean都是由接口+实现类完成的,用户想要获取Bean的实例直接从IoC容器获取就可用了,不需要关心实现类
image.png

Spring 主要有两种配置元数据的方式,一种是基于 XML、一种是基于 Annotation 方案的,目前主流的方案是基于 Annotation 的,这里也是以 Annotation 为基础方案来讲解

org.springframework.context.ApplicationContext接口类定义容器的对外服务,通过这个接口,我们可以轻松地从IoC容器中得到Bean对象。我们在启动Java程序的时候必须先要启动IoC容器

Annotation类型的IoC容器对应的类是org.springframework.context.annotation.AnnotationConfigApplicationContext
我们如果要启动 IoC 容器,可以运行下面的代码
ApplicationContext context = new AnnotationConfigApplicationContext("fm.douban");
这段代码的含义就是启动 IoC 容器,并且会自动加载包 fm.douban 下的 Bean,哪些 Bean 会被加载呢?只要引用了 Spring 注解的类都可以被加载(前提是在这个包下)

AnnotationConfigApplicationContext 这个类的构造函数有两种

  • AnnotationConfigApplicationContext(String ... basePackages) 根据包名实例化
  • AnnotationConfigApplicationContext(Class clazz) 根据自定义包扫描行为实例化

我们的例子就是第一种,两者根据情况做选择,开始的时候一般用第一种方案

Spring 官方声明为 Spring Bean 的注解有如下几种:

  • org.springframework.stereotype.Service
  • org.springframework.stereotype.Component
  • org.springframework.stereotype.Controller
  • org.springframework.stereotype.Repository

只要我们在类上引用这类注解,那么都可以被 IoC 容器加载

  • @Component注解是通用的 Bean 注解,其余三个注解都是扩展自Component
  • @Service正如这个名称一样,代表的是 Service Bean
  • @Controller作用于 Web Bean
  • @Repository作用于持久化相关 Bean

实际上这四个注解都可以被 IoC 容器加载,一般情况下,我们使用@Service;如果是 Web 服务就使用@Controller

  • 代码演示
package fm.douban;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import fm.douban.service.SongService;
import fm.douban.model.Song;

/**
 * Application
 */
public class Application {

  public static void main(String[] args) {

    ApplicationContext context = new AnnotationConfigApplicationContext("fm.douban");
    // 从容器中获取歌曲服务。
    SongService songService = context.getBean(SongService.class);
    // 获取歌曲
    Song song = songService.get("001");
    System.out.println("得到歌曲:" + song.getName());

  }
}

依赖注入的第一步是完成容器的启动,第二步就是真正地完成依赖注入行为了

  • 依赖注入这个词也是一种编程思想,简单来说就是一种获取其他实例的规范

我们定义一个接口的实现类SujectServiceImpl,如果我们想获取完整的专辑信息,就得引入 SongService 的实例,调用歌曲

public class SubjectServiceImpl implements SubjectService {

   private SongService songService;

   //缓存所有专辑数据
   private static Map<String, Subject> subjectMap = new HashMap<>();
   static {
       Subject subject = new Subject();
       //... 省略初始化数据的过程
       subjectMap.put(subject.getId(), subject);
   }

   @Override
   public Subject get(String subjectId) {
       Subject subject = subjectMap.get(subjectId);
       //调用 SongService 获取专辑歌曲
       List<Song> songs = songService.list(subjectId);
       subject.setSongs(songs);
       return subject;
   }

   public void setSongService(SongService songService) {
       this.songService = songService;
   } 
}
  • 那么,如何获取SongService的实例呢?

按照之前,我们需要一个外部工厂进行传递,调用setSongService方法传入进来。
现在,我们使用依赖注入

import fm.douban.model.Song;
import fm.douban.model.Subject;
import fm.douban.service.SongService;
import fm.douban.service.SubjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service
public class SubjectServiceImpl implements SubjectService {

    @Autowired
    private SongService songService;

    //缓存所有专辑数据
    private static Map<String, Subject> subjectMap = new HashMap<>();

    static {
        Subject subject = new Subject();
        subject.setId("s001");
        //... 省略初始化数据的过程
        subjectMap.put(subject.getId(), subject);
    }

    @Override
    public Subject get(String subjectId) {
        Subject subject = subjectMap.get(subjectId);
        //调用 SongService 获取专辑歌曲
        List<Song> songs = songService.list(subjectId);
        subject.setSongs(songs);
        return subject;
    }
}

做了三处改动:

第一处
第二处
第三处

在执行类中:

package fm.douban;

import fm.douban.model.Subject;
import fm.douban.service.SongService;
import fm.douban.service.SubjectService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * Application
 */
public class Application {

    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext("fm.douban");

        SubjectService subjectService = context.getBean(SubjectService.class);

        Subject subject = subjectService.get("s001");

        System.out.println("该专辑共有:"+subject.getSongs().size()+" 首歌");

    }
}

在这个例子中,当我们要用到其他接口的方法时,我们可以使用依赖注入来简化过程,不需要像之前一样再在外部工厂进行传递,调用setSongService方法传入进来
上面的例子其实已经是解释了依赖注入的工作模式,我们整理一下,你会发现依赖注入让我们得到其他 Bean 的实例相当简单,你只需要在属性上添加注解,就像下面的代码

@Autowired
private SongService songService;
  • Autowired 完整的类路径是 org.springframework.beans.factory.annotation.Autowired

当然还有一个前提条件,那就是当前的类是 Spring Bean 哦,比如这里我们添加了 @Service

  • 到目前为止,我们掌握了 Spring Bean 的知识

Spring Resource

文件系统是编程不可避开的领域,Spring Framework作为完整的Java企业级解决方案,自然也有文件处理方案,那就是Spring Resource

在正式学习Spring Resource之前,还需要了解一下在Java工程中文件的几种情况

  • 文件在电脑某个位置,d:/mywork/a.doc
  • 文件在工程目录下,比如mywork/toutiao.png
  • 文件在工程的src/main/resources 目录下,这是 Maven 工程存放文件的地方

第一种和第二种情况都是使用 File 对象就可以读写啦,第三种情况比较特殊,因为 Maven 执行 package 的时候,会把resources目录下的文件一起打包进 jar 包里(之前提到过 jar 是 Java 的压缩文件)。

显然在第三种情况,用 File 对象是读取不到的,因为文件已经在 jar 里了


image.png

Java 文件系统和计算机文件系统的差异,工程目录最后是要编译成 jar 文件的,jar文件是从包路径开始的,Maven 工程编译后,会启动去掉了 src/main/javasrc/main/resources 目录

那么现在的如何读取 jar 内部的文件呢?

src/main/resources目录的,会自动打包到 jar 文件内

classpath

在 Java 内部当中,我们一般把文件路径称为 classpath,所以读取内部的文件就是从 classpath 内读取,classpath 指定的文件不能解析成 File 对象,但是可以解析成 InputStream,我们借助 Java IO 就可以读取出来了

classpath 类似虚拟目录,它的根目录是从 / 开始代表的是 src/main/java或者src/main/resources目录

我们来看一下如何使用 classpath 读取文件,这次我们在 resources 目录下存放一个 data.json 文件。

  • Java 拥有很多丰富的第三方类库给我们使用,读取文件,我们可以使用 commons-io 这个库来,需要我们在 pom.xml 下添加依赖
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.6</version>
</dependency>

测试代码如下:

public class Test {

  public static void main(String[] args) {
    // 读取 classpath 的内容
    InputStream in = Test.class.getClassLoader().getResourceAsStream("data.json");
    // 使用 commons-io 库读取文本
    try {
      String content = IOUtils.toString(in, "utf-8");
      System.out.println(content);
    } catch (IOException e) {
      // IOUtils.toString 有可能会抛出异常,需要我们捕获一下
      e.printStackTrace();
    }
  }

}

再来认识一下这段代码

InputStream in = Test.class.getClassLoader().getResourceAsStream("data.json");

这段代码的含义就是从 Java 运行的类加载器(ClassLoader)实例中查找文件,Test.class指的当前的 Test.java 编译后的 Java class 文件

那么,Spring Resource可以做什么呢?
在Spring中定义了一个org.springframework.core.io.Resource来封装文件,这个类的优势在于可以支持普通的 File 也可以支持 classpath 文件。

并且在 Spring 中通过 org.springframework.core.io.ResourceLoader 服务来提供任意文件的读写,你可以在任意的 Spring Bean 中引入 ResourceLoader

@Autowired
private ResourceLoader loader;

现在让我们来看一下在 Spring 当中如何读取文件,我们创建一个自己的 FileService

public interface FileService {

    String getContent(String name);

}

实现类

import fm.douban.service.FileService;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;

@Service
public class FileServiceImpl implements FileService {

    @Autowired
    private ResourceLoader loader;

    @Override
    public String getContent(String name) {
        try {
            InputStream in = loader.getResource(name).getInputStream();
            return IOUtils.toString(in,"utf-8");
        } catch (IOException e) {
           return null;
        }
    }
}

看一下这个服务的调用

  FileService fileService = context.getBean(FileService.class);
  String content = fileService.getContent("classpath:data/urls.txt");
  System.out.println(content);

我们可以继续复用之前的代码,只是修改一下调用的文件目录

String content2 = fileService.getContent("file:mywork/readme.md");
System.out.println(content2);

这个file:mywork/readme.md代表的就是读取工程目录下文件mywork/readme.md

Resource 还可以加载远程文件,比如说

String content2 = fileService.getContent("https://www.zhihu.com/question/34786516/answer/822686390");
System.out.println(content2);

总结一下Spring Resource

在 Spring Resource 当中,把本地文件、classpath文件、远程文件都封装成 Resource 对象来统一加载,这就是它的强悍的地方

Spring Bean的生命周期

为了更好的管理 Bean,Spring Bean 提供了生命周期管理能力,这将极大的提高了工程化的能力

  • 什么叫生命周期呢?任何生命都有开始和结束的时候,生命从开始到结束的整个流程、状态就是生命周期。很多编程框架都提供生命周期管理,提供类似实例的开始=》结束的状态管理

    大部分时候,我们只需要掌握 init 方法即可,注意这个 init 方法名称可以是任意名称的,因为我们是通过注解来声明 init 的,我们以 SubjectServiceImpl 为例:
import javax.annotation.PostConstruct;

@Service
public class SubjectServiceImpl implements SubjectService {

  @PostConstruct
  public void init(){
      System.out.println("启动啦");
  }

}  

我们只要在方法上添加@PostConstruct注解,就代表该方法在 Spring Bean 启动后会自动执行

注意这个 PostConstruct 的完整包路径是javax.annotation.PostConstruct

有了 init 方法之后,我们就可以把之前 static 代码块的内容移到 init 里啦。所以代码就变成如下

@Service
public class SubjectServiceImpl implements SubjectService {

  @PostConstruct
  public void init(){
      Subject subject = new Subject();
      subject.setId("s001");
      subject.setName("成都");
      subject.setMusician("赵雷");

      subjectMap.put(subject.getId(), subject);
  }

}  
  • Spring 声明周期可以让我们更轻松的初始化一些行为以及维护数据
  • 暂时只要掌握这个初始化方法就好了

推荐阅读更多精彩内容