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.xml | Maven 的项目配置文件 |
${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 的一个歌曲网页
我们对这个歌曲页面做了一下简单的分析(完整的分析我们慢慢来),关注一下图中数字标注的点
-
歌曲所在专辑的封面
https://img3.doubanio.com/view/subject/m/public/s29114672.jpg
-
歌曲名称
成都
-
作者、专辑名称、专辑发布时间
- 作者:赵磊
- 专辑:成都
- 专辑发布时间:2016
-
本首歌的歌词
...太长了,省略
根据上面的分析,我们得出需要两个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,我们期望得到专辑的信息(包括专辑包含的歌曲信息)
-
我们仔细看一下我们的
Song
、Subject
这两个 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 工程中文件的几种情况:
- 文件在电脑某个位置,比如说
d:/mywork/a.doc
- 文件在工程目录下,比如说
mywork/toutiao.png
- 文件在工程的
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/java
、src/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 方法