Spring依赖注入

Spring简介

Spring 的核心

依赖注入(DI)是 Spring 最核心的技术点(我们在后面还会继续运用到),Spring 所有的技术方案都是基于 DI 来发展的。对于初学者来说理解 DI 会有一些困难,所以我们先要做到的是熟练使用 Spring 的技能。

使用 Spring 大大降低了开发难度和助力团队开发,它更加强调了面向对象,所以等大家习惯了之后你会发现就像喝开水一样简单。

在正式学习之前,希望大家先有一个概念:先熟练掌握它的要求,再去理解它的底层原理,要不然这门课会让你崩溃的哦,所谓实践出真知。

Maven 入门(上)

当我们开始学习 Java 框架的时候,就必须先掌握一个工程化工具:Apache Maven。

Apache Maven 是做什么用的?

Maven 是一个项目管理和构建自动化工具。但是对于我们程序员来说,我们最关心的是它的项目构建功能。所以这里我们介绍的就是怎样用 Maven 来满足我们项目的日常需要。如果说 Spring 是必须要学习的框架,那么 Maven 就是 Java 必须要掌握的工具。

Maven 提供了一个命令行工具可以把工程打包成 Java 支持的格式(比如 jar),并且支持部署到中央仓库里,这样使用者只需要通过工具就可以很快捷的运用其他人写的代码,只需要你添加依赖即可。

下图是一个 Maven 系统架构:

从这个架构里可以看到借助于中央仓库,可以把 Java 代码任意的共享给别人,这对于团队协同开发来说是至关重要的,可以说 Java 工程化发展到现在,Maven 起到了决定性的作用。

Maven 使用惯例优于配置的原则 。它要求在没有定制之前,所有的项目都有如下的结构(其实我们之前的 Java 工程目录都是遵循了这个结构规范):

目录目的
${basedir}存放 pom.xml 和所有的子目录
${basedir}/pom.xmlMaven 的项目配置文件
${basedir}/src/main/java项目的 java 源代码
${basedir}/src/main/resources项目的资源,比如说 property 文件
${basedir}/src/test/java项目的测试类,比如说 JUnit 代码
${basedir}/src/test/resources测试使用的资源

这里的 ${basedir} 代表的是 Java 工程的根路径,在我们这里就是工程的根目录啦。一个 Maven 项目在默认情况下会产生 JAR (Java 的一种压缩格式)文件,另外 ,编译后 的 classes 会放在 ${basedir}/target/classes 下面, JAR 文件会放在 ${basedir}/target 下面。

所以如果你的代码放错位置,那么程序是没办法完成编译、执行工作的。

Maven 命令

想要使用 Maven 这个工具,是要在命令行(终端软件)里输入指令的方式来执行的,我们需要大家了解几个常用的命令,注意命令要在工程的根目录下执行哦

1. mvn clean compile

编译命令,Maven 会自动扫描 src/main/java 下的代码并完成编译工作,执行完,会在根目录下生成 target/classes 目录(存放所有的 class)

2. mvn clean package

编译并打包命令,这个命令是 compile 和 package 的集合,也就是说会先执行 compile 命令,然后在执行 jar 打包命令,这个的结果会把所有的 java 文件和资源打包成一个jar,jar 是 java 的一个压缩格式,方便我们灵活的运用多个代码

3. mvn clean install

执行安装命令,这个命令是 compile 和 package、install 的集合,也就是说会先执行 compile 命令,然后在执行 jar 打包命令,然后执行 install 命令安装到本地的 Maven 仓库目录里,这个目录是 ${user_home}/.m2

这个 ${user_home} 指的就是你的电脑登录用户名的个人目录。这个操作基本和mvn clean package看起来一样。

4. mvn compile exec:java -Dexec.mainClass=${main}

这个命令的意思是在 compile 执行完后,执行运行 Java 的命令,具体执行哪个 Java 类是由 -Dexec.mainClass=${main} 参数指定的,比如我们想执行 com.youkeda.Test类,那么这个完整的命令就是:

mvn compile exec:java -Dexec.mainClass=com.youkeda.Test

总结

Maven 的命令非常强大,通过扩展插件可以支持更多的命令,不过我们掌握上面的几种也就足够啦。对于命令来说,我们只需要知道这个命令执行了什么操作,得到什么结果就可以了。

Maven 学习的重心还是掌握它的配置文件:pom.xml。

Maven 入门(中)

完成 Maven 安装、命令的了解后,接下来我们就开始学习 Maven 的核心概念

这五个概念都会运用在 Maven 的配置文件中。Maven 的配置文件是一个强约定的XML格式文件,它的文件名一定是pom.xml

我们掌握了 pom.xml 的格式规范以后,基本就可以灵活的运用 Maven 技术了。初学者可能会记不住这么多属性,没有关系,可以随时开启编程的无敌模式:拷贝、粘贴,照葫芦画瓢即可。

所以学习 Maven 我们只需要能够认识 pom.xml 内容就可以啦,不要试图一下子就完全掌握,我们还是把宝贵的时间留给代码,工具多用用自然就会熟悉啦。

1、POM (Project Object Model)

一个 Java 项目所有的配置都放置在 POM 文件中,大概有如下的行为:

  • 定义项目的类型、名字
  • 管理依赖关系
  • 定制插件的

可以看一下下图,有个大概的了解:

分别看一下上图的内容

  • 1、Maven 坐标
  • 2、Maven 工程属性
  • 3、Maven 依赖
  • 4、Maven 插件

补充介绍一下,XML 格式可能是我们第一次接触,其实 HTML 语言也是 XML 格式,不过 XML 格式会严格遵守标记语言的要求,那就是有开始标签和结束标签,比如 <version>1.0</version>,我们在自定义 pom.xml 时候,如果没有写完整的开始、结束标签,那就会出错,这里我们就不详细展开了,大家多看几次也就了解啦。

1.1 Maven 坐标
<groupId>com.youkeda.course</groupId>
<artifactId>app</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>

这四个标签组成了 Maven 的坐标,所谓坐标就是一种位置信息,Maven 的坐标决定了这个 Maven 工程部署后存在 Maven 仓库的文件位置,所以这个坐标信息是必须要指定的。

groupId

groupId 就像一个文件夹一样,它的命名和 Java 的包比较一致,这里一般只用小写的英文字母和字符.,比如这里的com.youkeda.course。一般来说一个公司会设置自己的 groupId,避免和其他公司重合,个人开发者也一样。

artifactId

artifactId 有点像文件名一样,在一个 groupId 内,它应该是唯一的,你不能使用中文或者特殊字符,从规范上来说只能使用小写的英文字母、.-_。比如:app、member.shared 这些都可以

packaging

Maven 工程执行完后会把整个工程打包成packaging指定的文件格式,默认情况下packaging的值是jar,所以如果pom.xml文件中没有声明这个标签,那就是 jar

packaging 有如下的几种格式

  • jar
  • war
  • ear
  • pom

多数情况下,我们使用的都是jar ,其他格式我们在以后会逐步运用到,这里有个概念就好。

version

version 很有意思的,它基本上遵守了软件工程中对版本号的约定。

在 Maven 的世界里,会把一个工程分为两个状态,这也是软件工程里最最常用的规范。

  • SNAPSHOT 这个单词翻译过来的意思是快照,实际上代表了当前程序还处于不稳定的阶段,随时可以再修改,所以在我们开发的时候我们会在版本号后面加上SNAPSHOT关键字

  • RELEASE RELEASE 和 SNAPSHOT 是对立面的,所以它代表的就是稳定,一般我们正式发布的时候,都会把 version 改为RELEASE。当然你可以不用特意的加上RELEASE,因为只要不是SNAPSHOT,那就是RELEASE

了解了工程状态之后,我们再来看版本号的约定,在软件工程里,我们一般会用三位数字来表示版本号,所以大概是x.x.x这样的格式,比如说 iPhone 11 搭配的操作系统的版本是iOS 13.1.2,正如你所想的这是一个 RELEASE 版本

三位版本号如何使用呢,也是有规则的

  • 第一位代表的是主版本号:主版本号一般是团队约定来的,上面 iOS 的例子中13就是主版本号。

  • 第二位代表的是新增功能:上面例子中的1代表的就是新增功能后的版本,这个数字表明苹果在 13 这个版本里,有了 1 次新增功能的行为。

  • 第三位代表的是 bugfix 后的版本:bugfix 是修复代码缺陷、bug 的行为,我们知道苹果经常会发布一些版本用于修复问题,所以上面的2就是 bugfix 版本,从这个数字你可以大致判断,苹果做了两次 bug 修复。

有些时候,我们也有可能用两位版本,那就是没有第一位的主版本号(因为某些时候,可能不需要主版本号)具体选用哪种根据团队的情况来做选择,不用纠结,大家约定好就行。

在编程过程中约定大于一切,请大家记住这句话。

软件的版本大多时候是从 1.0.0开始的,在开发状态下那就是 1.0.0-SNAPSHOT,请注意这个格式[version]-SNAPSHOT,不能写错啦。

如果你新增了功能,那么版本号就是1.1.0-SNAPSHOT,如果你要正式发布了,那么版本号就是1.1.0

一般来说每一位的最大值就是100,所以不要超过100哦,印象中只有微软操作系统的版本号才超大,因为它的历史悠久功能特别特别多。。。

最后还有一个约定,那就是执行 mvn package、mvn install 命令生成的 jar 文件名是[artifactId]-[version].jar,你可以看看刚才的演示执行的 target 目录下是不是有一个 app-1.0.0-SNAPSHOT.jar文件

1.2 Maven 属性配置

Maven 的属性配置就是用来做参数设置的,如下的配置是我们经常看到的

 <properties>
    <java.version>1.8</java.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

首先它的格式是在properties 标签内,这个是固定的格式。properties内的标签可以自定义,但是一般来说只能是小写英文字母+.

当然默认也有一些公共参数是可以调整的,比如我们这里的

  • java.version

代表设置一个参数:java.version,它的值是1.8

  • maven.compiler.source

这个参数是指定 Maven 编译时候源代码的 JDK 版本, ${java.version} 这个值有点特殊,它是一个动态值,${key}这个语法会动态找到key这个参数配置的值,所以上面的例子中${java.version}的实际值是1.8

  • project.build.sourceEncoding

这个参数指定的是工程代码源文件的文件编码格式,一般情况下,我们都设置成UTF-8

  • maven.compiler.target

这个参数作用是是按照这个值来进行编译源代码,比如这里的例子是按照 JDK1.8 进行编译

总结

Maven作为一个工具来说,概念和知识点还是有一些的,所以初学者先有概念即可,随着多次使用,自然会熟悉起来,因为知识点内容比较多,所以一下子也不太会记得全,但是我们在之后的工程里都会运用到的,所以很自然的孰能生巧。

Maven 入门(下)

Maven 这个工具最有价值的地方,那就是依赖管理和插件体系。掌握了 Maven 坐标,有了 Maven 坐标我们就可以通过 Maven 的依赖管理来运用其他人的代码。

1、依赖管理 dependencies

dependency 就是用于指定当前工程依赖其他代码库的,Maven 会自动管理 jar 依赖。

一旦我们在 pom.xml 里声明了 dependency 信息,会先去本地用户目录下的.m2文件夹内查找对应的文件,如果没有找到那么就会触发从中央仓库下载行为,下载完会存在本地的.m2文件夹内。

我们只需要在 pom.xml 里添加标签即可。首先声明一个父标签 dependencies,然后就可以在这个标签内部添加依赖了。比如我们之前学习到使用 fastjson 这个库

<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
</dependencies>

请注意,一个 pom.xml 只能存在一个 dependencies 标签,可以有多个 dependency,因为我们很有可能依赖多个库。大家仔细观察这个dependency标签,你会发现dependency标签的内容其实就是 Maven 坐标,所以说只要有坐标我们就可以建立依赖。

当然你可以在 dependencies 内添加多个依赖,比如说继续添加 okhttp3 依赖

<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.2.2</version>
    </dependency>
</dependencies>

一般我们会把别人写的代码库称为三方库,自己、团队写的称为二方库,这个概念请大家记住,以后我们可能会这样描述内容的。

1.1 中央仓库

前面我们提到过 Maven 会把所有的 jar 都存放在中央仓库里,我们可以通过 Maven Central Repository Search 这个网站来搜索 jar。可惜的是这个网站部署在国外,在国内访问不到,我们可以访问阿里云的镜像服务器 仓库服务

1.2 间接依赖

间接依赖是 mvn 成功的核心要素,简单的来说,如果一个remote工程依赖了 okhttp 库,而当前工程locale依赖了 remote工程,这个时候locale工程也会自动依赖了 okhttp ,省时又省力是不是?

2、插件体系 plugins

插件体系让 Maven 这个工具变得高度可定制,所以可以无缝的支撑工程化能力。现在只要了解插件的格式就可以啦,因为不同的插件有不同的作用,比如说

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
        </plugin>
    </plugins>
</build>

这里声明了一个 maven-compiler-plugin 插件用于执行 maven compile 的,你会发现 maven 的插件其实也是存放在中央仓库的坐标,也就是一切都是 jar。

关于插件,大家能够看懂,知道如何添加即可,不用特别纠结概念,因为不同的插件是配合不同的工程来设定的。现在深究它意义不大,等我们用了很多工程的时候,自然会明白了。

总结

到目前位置,我们的 maven 入门就告一段落了,我们在后面的课程中还会陆续学习到 maven 的知识,工具只有多用才能更容易理解。所以我们这里也只是先解决最核心的知识点,其他的知识点,随着我们工程的复杂而逐步深入。

Hello Spring

搞定了前置的技术条件后,我们来配置、创建一个 Spring 工程吧。目前来说,我们基本上使用的是 Spring5

Spring5 的Maven坐标如下

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

请大家记住一点:Spring 强调的是面向接口编程,所以大多数情况下 Spring 代码都会有接口和实现类。

用 Spring 来看一个 Hello World 例子:

package com.youkeda;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import com.youkeda.service.MessageService;

/**
 * Application
 */
@ComponentScan
public class Application {

    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        MessageService msgService = context.getBean(MessageService.class);
        System.out.println(msgService.getMessage());

    }
}

是不是有点复杂???嗯,对于刚学完 Java 的同学来看确实有点复杂,因为这几行代码里面体现了很多的架构、设计思想,对于很多新手来说掌握 Spring 的难度都是如同攀登一座高山一样,所以在接下来的学习过程中会遇到一些想不明白的代码,只要你记住学习编程最重要的一点:那就是先会用再去理解

在刚才的代码,有用到一些知识点:

  • 注解
  • Spring Bean
  • Spring 扫描
  • Spring 声明周期

当我们理解了这些知识的时候,上面的代码的含义大概就可以明白了,这些疑问我们下一章见

面向接口的理解

上面的代码,我们希望大家可以感受一下使用 Spring 之后面向接口开发的不同点

上图中实际上演示的例子的工作就是调用了 MessageService 实例的 getMessage() 方法。

看起来好像是 Java 方式更简单点,但是大家仔细对比一下,你会发现在 Spring 当中,我们如果想调用MessageService就可以直接从上下文获取,而不需要关心它的实现类、如何实例化实现类,从而达到了真正的解耦合。

这就像我们日常中使用 U 盘,只要确定它是 USB 接口就知道可以用,管它到达是谁、怎么生产来的。

当代码越多的时候,Spring 的效率就会体现出来,比原生 Java 开发要快很多倍

所以这也是 Spring 最大的价值,完全的屏蔽了实现细节,让使用者可以专注与接口的定义和运用即可,这对多人协同开发的时候非常非常有用,屏蔽实现方式也就意味着降低了工程的复杂度,因为只要开发双方约定好接口就可以一起工作,而不是你在这里写一段代码,我在那里写一段代码,完全乱套了。

在后面,我们学习到 Spring 更多知识和大型项目的时候,大家会对这个概念有更深的理解,现在不理解也没关系。

Spring依赖注入

 Java 注解(Annotation)

回顾一下前面的例子代码

@Service
public class MessageServiceImpl implements MessageService{

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

}

这段代码很平常,但是多一个语法@Service,这个语法就是我们今天要学习的注解。

Annotation (注解)

本质上来说 Annotation(注解) 是 Java 推出的一种注释机制,后面我们统一叫 Annotation,和普通的注释有个显著的特别,那就是 Annotation 是可以在编译、运行阶段读取的,注释很明显不可以。

能够在编译、运行阶段读取信息,这就给了我们很多的扩展空间,而且不会污染源代码。我们可以借助它来实现一些增强功能,在 Spring 当中就重度使用了 Annotation,通过运行阶段动态的获取 Annotation 从而完成很多自定义的行为,所以如果不了解 Annotation,那学习 Spring 就没有办法进行下去了。

从另外一个角度来看,Annotation 也是一个 Java 类,只是这个类太特殊了点。我们可以先看看上面的 Service 这个 Annotation 的源代码

我们需要掌握这 5 个小点,就理解了 Annotation 的工作机制了。

上面 Service 注解的作用是声明引用该注解的 Java 类为 Service 对象,还有一个点,大家可能已经发现了,Annotation 类里可以继续引用其他的 Annotation 类。所以一个 Annotation 是由多个 Annotation 组合而成的

1、Target

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

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

  • ElementType.TYPE

    可以作用于 类、接口类、枚举类上

  • ElementType.FIELD

    可以作用于类的属性上

  • ElementType.METHOD

    可以作用于类的方法上

  • ElementType.PARAMETER

    可以作用于类的参数

如果想要同时作用于类和方法上,那么你就可以

@Target({ElementType.TYPE,ElementType.METHOD})

如果某个 Annotation 的 Target 设定为 METHOD ,那么你就只能在方法前面上面引用它,其他的地方都不行,其他 Target 值也类似这个规则。

我们再看几个例子来区分一下,

1.1 ElementType.TYPE
@Service
public class MessageServiceImpl implements MessageService{

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

}
1.2 ElementType.METHOD

public class MessageServiceImpl implements MessageService{

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

}
1.3 ElementType.FIELD

public class MessageServiceImpl implements MessageService{

    @Autowired
    private WorkspaceService workspaceService;

}
1.4 ElementType.PARAMETER

public class MessageServiceImpl implements MessageService{

    public String getMessage(@RequestParam("msg")String msg) {
         return "Hello "+msg;
    }

}

大家暂时只要关注语法就好了,具体的注解类的含义,我们后面会逐步学习到的

2、Retention

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

1.SOURCE: 也就是说是纯注释作用 2.CLASS: 也就是在编译阶段是有效的 3.RUNTIME: 在运行时有效

@Retention(RetentionPolicy.RUNTIME)

这个代码表示的就是在运行期间有效

如果是我们自己定义的 Annotation ,一般我们都会设置成 RUNTIME

3、Documented

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

4、@interface

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

5、Annotation 属性
String value() default "";

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

有了这个属性就可以正式的引用一个 Annotation ,比如:

import org.springframework.stereotype.Service;

@Service
public class Demo {

}

注意,Annotation 也是 Java 类,所以一样需要 import 的。

上面的@Service 也可以写成

import org.springframework.stereotype.Service;

@Service(value="Demo")
public class Demo {

}

因为 value 是默认属性名称是可以缩写的,所以上面的代码等同于

import org.springframework.stereotype.Service;

@Service("Demo")
public class Demo {

}

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")这个是别名的意思,就是说这个属性用这个别名也可以访问到,所以下面的代码是一样的意思

@RequestParam("key")
@RequestParam(value="key")
@RequestParam(name="key")

具体的代码如下:

package com.youkeda.service.impl;

import com.youkeda.service.MessageService;
import org.springframework.stereotype.Service;

@Service
public class MessageServiceImpl implements MessageService{

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

}

总结

在现在这个阶段,我们只要能够认识和引用 Annotation(注解)就可以啦,在以后随着项目的难度提升,我们可以考虑自定义注解类。

需求分析

我们看一下豆瓣fm 的一个歌曲网页

我们对这个歌曲页面做了一下简单的分析(完整的分析我们慢慢来),关注一下图中数字标注的点

  1. 歌曲所在专辑的封面

    https://img3.doubanio.com/view/subject/m/public/s29114672.jpg

  2. 歌曲名称

    成都

  3. 作者、专辑名称、专辑发布时间

    • 作者:赵磊
    • 专辑:成都
    • 专辑发布时间:2016
  4. 本首歌的歌词

    ...太长了,省略

根据上面的分析,我们得出需要两个POJO 类

  • Song.java 歌曲类
  • Subject.java 专辑类

如果我们想浏览歌曲的信息,那么我们得需要一个歌曲服务

  • SongService.java

OK,现在我们设计了一个 UML 图

代码要求

  • 根据 UML 图,完成代码的编写

  • 给 SongServiceImpl 类添加类注解 @Service

    注意 Service 的完整包路径是 org.springframework.stereotype.Service

  • 需要初始化缓存歌曲数据,默认的代码数据如下,其余的代码自己完善

    static {
        Song song = new Song();
        song.setId("001");
        song.setSubjectId("s001");
        song.setLyrics("...");
        song.setName("成都");
    }
    
  • Application.java 是系统默认创建的类,不需要修改,可以用来运行工程

扩展知识点:静态代码块

有时候,为了让系统能够自动执行一些代码,可以采用静态代码块的方式:

public class Hello {
  static {
    Song song = new Song();
    ... ...
    ... ...
  }
  ... ...
}

非完整代码,仅用于演示静态代码块语法

静态代码块的语法比较特殊,不像方法一样,没有参数也没有返回值。只要在类代码中写 static {} 就好,系统加载这个类的时候,会自动执行静态代码块中的代码。

要注意的是,既然称之为“静态”代码块,就意味着无论这个类被实例化多少次(new Hello()),静态代码块都只执行一次。

记住:静态代码块要写在类中,而不能写在方法中

静态代码块可以有多个,位置也可以随便放。但是一般来说,静态代码块推荐写在属性声明语句之后,方法声明之前,这样能尽快阅读到,尽快了解类的这些功能逻辑。

如果静态代码块有多个,系统将按照它们在类中书写的先后顺序依次执行它们

public class Hello {
  private static Map<String, Song> songMap = new HashMap<>();

  static {
    Song song = new Song();
    song.setId("001");
    songMap.put(song.getId(), song);
  }

  public Song getSong() {

  }
}

静态代码块比较适合这种需要初始化数据的场景。

Spring Bean(上)

IoC(Inversion of Control,控制反转) 容器是 Spring 框架最最核心的组件,没有 IoC 容器就没有 Spring 框架。作为面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

在 Spring 框架当中,主要通过依赖注入(Dependency Injection,简称DI)来实现 IoC。

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

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");
        // 实例化 IoC 容器
        SongService songService=context.getBean(SongService.class);
        // 获取 SongService
        Song song=songService.get("001");
        // 调用 songService.get("001") 方法,得到 Song
        System.out.println("得到歌曲:"+song.getName()+song.getId());
        // 打印 Song 的id和名称
    }
}

Spring Bean(下)

IoC 容器就像一个大型的工厂一样,我们不关心工厂如何生产,只需使用工厂生产的产品。

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

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

我们还是以豆瓣为例,来学习和理解依赖注入思想

  • 我们在前面的作业完成了豆瓣歌曲服务的定义,听过歌的同学应该都知道,有歌曲就有专辑,我们可以通过一个专辑获取这专辑单包含的歌曲,看一下 UML 图

  • 我们新增了一个 SubjectService 和它的实现类 SubjectServiceImpl,用来完成获取专辑的服务,在这个接口里我们定义了一个 get 方法,传入参数为 subjectId,我们期望得到专辑的信息(包括专辑包含的歌曲信息)

  • 我们仔细看一下我们的 SongSubject 这两个 POJO 类,从这个模型上来看,我们如果去带着 Subject 的 id 去循环遍历所有的歌曲,筛选出来的歌曲应该就是专辑包含的歌曲。所以我们在 SongService 里又新增了一个 list 方法用于查询专辑歌曲

  • 回到 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;
    }
}

我们做了三处改动

运行一下:

[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ app ---
该专辑共有:1 首歌
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

改动前 VS 改动后

存在的问题(改动前)

任何需要使用 SubjectService 的地方,都需要编码实例化对象:

SubjectService subjectService = new SujectServiceImpl();
SongService songService = new SongServiceImpl();
subjectService.setSongService(songService);

那么问题来了,万一 SujectServiceImpl 依赖的 XXXService 太多,比如七八上十个,那么这段代码就非常长,就需要大量编码、 new 很多服务实例。

脑补一下 SujectServiceImpl 依赖 10 个其它服务,代码需要写多少行

在课程中服务关系,为了便于理解,就没有设计太复杂;但是实际企业的项目中,服务之间的依赖关系可能数量多、路径深:

所以,大量编码 new 出服务实例,会导致整个项目极其容易出错,包括各种笔误、遗漏等;而且代码太多,检查代码也比较困难。

例如,如果遗漏了 new SongServiceImpl() ,那么 songService.list() 就会出现空指针异常

学习过程中出错,比较容易修正。但企业开发时,仅仅修改错误是不够的,更加需要的是防止出错的方法、机制。

如何解决问题(改动后)

加注解的作用,是让 Spring 系统 自动 管理 各种实例。

所谓 管理 ,就是用 @Service 注解把 SubjectServiceImpl 和 SongServiceImpl 等等所有服务实现,都标记成 Spring Bean ;然后,在任何需要使用服务的地方,用 @Autowired 注解标记,告诉 Spring 这里需要注入实现类的实例。

项目启动过程中,Spring 会 自动 实例化服务实现类,然后 自动 注入到变量中,不需要开发者大量的写 new 代码了,就解决了上述的开发者需要大量写代码而导致容易出错的问题。

@Service 和 @Autowired 是相辅相成的:如果 SongServiceImpl 没有加 @Service ,就意味着没有标记成 Spring Bean ,那么即使加了 @Autowired 也无法注入实例;而 private SongService songService; 属性忘记加 @Autowired ,Spring Bean 亦无法注入实例。二者缺一不可。

每个 Annotation (注解)都有各自特定的功能,Spring 检查到代码中有注解,就自动完成特定功能,减少代码量、降低系统复杂度。

初学 Spring ,需要 理解 并 牢记 常见的这些注解的功能和作用。

依赖注入小结

OK,上面的例子其实已经是解释了依赖注入的工作模式,我们整理一下,你会发现依赖注入让我们得到其他 Bean 的实例相当简单,你只需要在属性上添加注解,就像下面的代码

@Autowired
private SongService songService;

Autowired 完整的类路径是 org.springframework.beans.factory.annotation.Autowired

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

到目前为止,我们掌握了 Spring Bean 的知识,可以改进一些代码啦,让我们继续实战下去,你会发现 Spring 的道理比较复杂,但是运用起来其实很简单,因为这是各种设计模式综合运用的结果。以后我们也会逐步的了解设计模式的,到时候再精进 Spring 会更好的理解,现在还是让我们熟练的使用它。

Spring Resource(上)

文件系统是编程不可避开的领域,因为我们总是有可能读文件、写文件。Spring Framework 作为完整的 Java 企业级解决方案,自然也有文件处理方案啦,那就是 Spring Resource。

在我们正式学习 Spring Resource 的时候,还需要了解一下在 Java 工程中文件的几种情况:

  1. 文件在电脑某个位置,比如说 d:/mywork/a.doc
  2. 文件在工程目录下,比如说 mywork/toutiao.png
  3. 文件在工程的src/main/resources 目录下,我们在 Maven 的知识里介绍过这是 Maven 工程存放文件的地方

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

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

看一下下图:

我们可以通过代码演示一下这个差别

import java.io.File;

/**
 * Test
 */
public class Test {

  public static void main(String[] args) {

    File file = new File("mywork/readme.md");
    if(file.exists()){
      System.out.println("readme.md 文件存在");
    }

    File file2 = new File("src/main/resources/nani.jpeg");
    if(file2.exists()){
      System.out.println("nani.jpeg 文件存在");
    }

  }

}

大家看到控制台输出了两个文件,现在看起来两个文件都可以加载对把?但是呢,在企业中源代码是不可能和运行程序放在一起的,所以下面的工程才是真实的情况。

我们运行看一下,这时候你会发现src/main/resources/nani.jpeg 图片找不到啦

大家可以下载app-1.0-SNAPSHOT.jar到本地电脑,然后用解压缩软件解压看看文件

咦???解压后,我们刚才那个图片在文件夹根目录下呢?并且src/main/java这些都没有啦。

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

OK,那么现在的问题就是如何读取 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 文件。记住这个代码格式就好,以后可能会经常用到,如果你记不住,那就记住在这节课程里有这个语法,copy 也可以。

Spring Resource(下)

了解了 classpath 概念后,我想你肯定在想 Spring Resource 能做啥呢?请记住一点,Spring 擅长的就是封装各种服务

在 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 当中,把本地文件、classpath文件、远程文件都封装成 Resource 对象来统一加载,这就是它的强悍的地方。

package fm.douban.service;
public interface FileService {
    String getContent(String name);
}

package fm.douban.service;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import java.io.InputStream;
@Service
public class FileServiceImpl implements FileService {
    @Autowired
    private ResourceLoader resourceLoader;
    @Override
    public String getContent(String name) {
        try {
            InputStream inputStream = resourceLoader.getResource(name).getInputStream();
            return IOUtils.toString(inputStream,"utf-8");
        }catch (Exception e){
            return null;
        }
    }
}

package fm.douban;
import fm.douban.service.FileService;
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");
        FileService fileService = context.getBean(FileService.class);
        // TODO 调用 getContent 方法
        String content = fileService.getContent("http://news.baidu.com/");
        System.out.println(content);
    }
}

Spring Bean 的生命周期(Lifecycle)

为了更好的管理 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 声明周期可以让我们更轻松的初始化一些行为以及维护数据,现在这个阶段大家暂时只要掌握这个初始化方法就好了,随着项目的复杂,我们会逐步理解到其深层次的道理的。

package fm.douban.service.impl;

import fm.douban.model.SongList;
import fm.douban.service.SongListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import javax.annotation.PostConstruct;
import org.apache.commons.io.IOUtils;
import com.alibaba.fastjson.JSON;
import java.io.InputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SongListServiceImpl implements SongListService {

    // 缓存所有歌单数据
    private static Map<String, SongList> songListMap = new HashMap<>();

    @Autowired
    private ResourceLoader loader;

    @PostConstruct
    public void init() {

        try {
            InputStream in = loader.getResource("classpath:data/songlists.json").getInputStream();
            String jsonString = IOUtils.toString(in, "utf-8");
            SongList[] lists = JSON.parseObject(jsonString, SongList[].class);
            for(SongList songList : lists){
                songListMap.put(songList.getId(),songList);
            }
        } catch (IOException e) {
            e.fillInStackTrace();
        }

    }


    @Override
    public SongList get(String songListId) {
        return songListMap.get(songListId);
    }
}

package fm.douban;

import fm.douban.model.SongList;
import fm.douban.service.SongListService;
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");

        SongListService songListService = context.getBean(SongListService.class);
        SongList songList = songListService.get("41299401");
        
        System.out.println("歌单:"+songList.getName());

    }
}

现在很多音乐网站都有歌单服务,歌单是由用户自己创建的,豆瓣FM也包含歌单功能的

根据上图,我们设计了一下歌单和歌单服务的领域模型

代码要求

现在在工程里提供了一个 songlists.json 文件(JSON 格式),里面存放的是歌单数据。

  • 在 SongListServiceImpl 类中创建一个初始化方法 init,使用 Spring 的初始化注解
  • 在 init 方法中使用 FastJson 库加载这个 songlists.json 文件
  • 创建一个 Map 的静态变量,存储从 json 文件读取到的数据
  • 完成 SongListServiceImpl 的 get 方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值