Spring 学习,看松哥这一篇万余字干货就够了!
1. Spring 简介
我们常说的 Spring 实际上是指 Spring Framework,而 Spring Framework 只是 Spring 家族中的一个分支而已。那么 Spring 家族都有哪些东西呢?
Spring 是为了解决企业级应用开发的复杂性而创建的。在 Spring 之前,有一个重量级的工具叫做 EJB,使用 Spring 可以让 Java Bean 之间进行有效的解耦,而这个操作之前只有 EJB 才能完成,EJB 过于臃肿,使用很少。Spring 不仅仅局限于服务端的开发,在测试性和松耦合方面都有很好的表现。
一般来说,初学者主要掌握 Spring 四个方面的功能:
Ioc/DI
AOP
事务
JdbcTemplate
2. Spring 下载
正常来说,我们在项目中添加 Maven 依赖就可以直接使用 Spring 了,如果需要单独下载 jar,下载地址如下:
https://repo.spring.io/libs-release-local/org/springframework/spring/5.2.1.RELEASE/
下载成功后,Spring 中的组件,大致上提供了如下功能:
3.1 Ioc
3.1.1 Ioc 概念
Ioc (Inversion of Control),中文叫做控制反转。这是一个概念,也是一种思想。控制反转,实际上就是指对一个对象的控制权的反转。例如,如下代码:
public class Book {
private Integer id;
private String name;
private Double price;
//省略getter/setter
}
public class User {
private Integer id;
private String name;
private Integer age;
public void doSth() {
Book book = new Book();
book.setId(1);
book.setName("故事新编");
book.setPrice((double) 20);
}
}
在这种情况下,Book 对象的控制权在 User 对象里边,这样,Book 和 User 高度耦合,如果在其他对象中需要使用 Book 对象,得重新创建,也就是说,对象的创建、初始化、销毁等操作,统统都要开发者自己来完成。如果能够将这些操作交给容器来管理,开发者就可以极大的从对象的创建中解脱出来。
使用 Spring 之后,我们可以将对象的创建、初始化、销毁等操作交给 Spring 容器来管理。就是说,在项目启动时,所有的 Bean 都将自己注册到 Spring 容器中去(如果有必要的话),然后如果其他 Bean 需要使用到这个 Bean ,则不需要自己去 new,而是直接去 Spring 容器去要。
通过一个简单的例子看下这个过程。
3.1.2 Ioc 初体验
首先创建一个普通的 Maven 项目,然后引入 spring-context 依赖,如下:
org.springframework
spring-context
5.1.9.RELEASE
接下来,在 resources 目录下创建一个 spring 的配置文件(注意,一定要先添加依赖,后创建配置文件,否则创建配置文件时,没有模板选项):
在这个文件中,我们可以配置所有需要注册到 Spring 容器的 Bean:
class 属性表示需要注册的 bean 的全路径,id 则表示 bean 的唯一标记,也开可以 name 属性作为 bean 的标记,在超过 99% 的情况下,id 和 name 其实是一样的,特殊情况下不一样。
接下来,加载这个配置文件:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}
执行 main 方法,配置文件就会被自动加载,进而在 Spring 中初始化一个 Book 实例。此时,我们显式的指定 Book 类的无参构造方法,并在无参构造方法中打印日志,可以看到无参构造方法执行了,进而证明对象已经在 Spring 容器中初始化了。
最后,通过 getBean 方法,可以从容器中去获取对象:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = (Book) ctx.getBean("book");
System.out.println(book);
}
}
加载方式,除了ClassPathXmlApplicationContext 之外(去 classpath 下查找配置文件),另外也可以使用 FileSystemXmlApplicationContext ,FileSystemXmlApplicationContext 会从操作系统路径下去寻找配置文件。
public class Main {
public static void main(String[] args) {
FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("F:\\workspace5\\workspace\\spring\\spring-ioc\\src\\main\\resources\\applicationContext.xml");
Book book = (Book) ctx.getBean("book");
System.out.println(book);
}
}
3.2 Bean 的获取
在上一小节中,我们通过 ctx.getBean 方法来从 Spring 容器中获取 Bean,传入的参数是 Bean 的 name 或者 id 属性。除了这种方式之外,也可以直接通过 Class 去获取一个 Bean。
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = ctx.getBean(Book.class);
System.out.println(book);
}
}
这种方式有一个很大的弊端,如果存在多个实例,这种方式就不可用,例如,xml 文件中存在两个 Bean:
此时,如果通过 Class 去查找 Bean,会报如下错误:
所以,一般建议使用 name 或者 id 去获取 Bean 的实例。
3.3 属性的注入
在 XML 配置中,属性的注入存在多种方式。
3.3.1 构造方法注入
通过 Bean 的构造方法给 Bean 的属性注入值。
1.第一步首先给 Bean 添加对应的构造方法:
public class Book {
private Integer id;
private String name;
private Double price;
public Book() {
System.out.println("-------book init----------");
}
public Book(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
}
2.在 xml 文件中注入 Bean
这里需要注意的是,constructor-arg 中的 index 和 Book 中的构造方法参数一一对应。写的顺序可以颠倒,但是 index 的值和 value 要一一对应。
另一种构造方法中的属性注入,则是通过直接指定参数名来注入:
如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。
3.3.2 set 方法注入
除了构造方法之外,我们也可以通过 set 方法注入值。
set 方法注入,有一个很重要的问题,就是属性名。很多人会有一种错觉,觉得属性名就是你定义的属性名,这个是不对的。在所有的框架中,凡是涉及到反射注入值的,属性名统统都不是 Bean 中定义的属性名,而是通过 Java 中的内省机制分析出来的属性名,简单说,就是根据 get/set 方法分析出来的属性名。
3.3.3 p 名称空间注入
p 名称空间注入,使用的比较少,它本质上也是调用了 set 方法。
3.3.4 外部 Bean 的注入
有时候,我们使用一些外部 Bean,这些 Bean 可能没有构造方法,而是通过 Builder 来构造的,这个时候,就无法使用上面的方式来给它注入值了。
例如在 OkHttp 的网络请求中,原生的写法如下:
public class OkHttpMain {
public static void main(String[] args) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.get()
.url("upload/201912171117318315.jpg")
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println(e.getMessage());
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
FileOutputStream out = new FileOutputStream(new File("E:\\123.jpg"));
int len;
byte[] buf = new byte[1024];
InputStream is = response.body().byteStream();
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.close();
is.close();
}
});
}
}
这个 Bean 有一个特点,OkHttpClient 和 Request 两个实例都不是直接 new 出来的,在调用 Builder 方法的过程中,都会给它配置一些默认的参数。这种情况,我们可以使用 静态工厂注入或者实例工厂注入来给 OkHttpClient 提供一个实例。
1.静态工厂注入
首先提供一个 OkHttpClient 的静态工厂:
public class OkHttpUtils {
private static OkHttpClient OkHttpClient;
public static OkHttpClient getInstance() {
if (OkHttpClient == null) {
OkHttpClient = new OkHttpClient.Builder().build();
}
return OkHttpClient;
}
}
在 xml 文件中,配置该静态工厂:
这个配置表示 OkHttpUtils 类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。然后,在 Java 代码中,获取到这个实例,就可以直接使用了。
public class OkHttpMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
OkHttpClient okHttpClient = ctx.getBean("okHttpClient", OkHttpClient.class);
Request request = new Request.Builder()
.get()
.url("upload/201912171117318315.jpg")
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println(e.getMessage());
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
FileOutputStream out = new FileOutputStream(new File("E:\\123.jpg"));
int len;
byte[] buf = new byte[1024];
InputStream is = response.body().byteStream();
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.close();
is.close();
}
});
}
}
2.实例工厂注入
实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法。
这次的工厂类如下:
public class OkHttpUtils {
private OkHttpClient OkHttpClient;
public OkHttpClient getInstance() {
if (OkHttpClient == null) {
OkHttpClient = new OkHttpClient.Builder().build();
}
return OkHttpClient;
}
}
此时,在 xml 文件中,需要首先提供工厂方法的实例,然后才可以调用工厂方法:
自己写的 Bean 一般不会使用这两种方式注入,但是,如果需要引入外部 jar,外部 jar 的类的初始化,有可能需要使用这两种方式。
3.4 复杂属性的注入
3.4.1 对象注入
可以通过 xml 注入对象,通过 ref 来引用一个对象。
3.4.2 数组注入
数组注入和集合注入在 xml 中的配置是一样的。如下:
足球
篮球
乒乓球
注意,array 节点,也可以被 list 节点代替。
当然,array 或者 list 节点中也可以是对象。
注意,即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。
3.4.3 Map 注入
3.4.4 Properties 注入
99
javaboy
以上 Demo,定义的 User 如下:
public class User {
private Integer id;
private String name;
private Integer age;
private Cat cat;
private String[] favorites;
private List cats;
private Map map;
private Properties info;
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", cat=" + cat +
", favorites=" + Arrays.toString(favorites) +
", cats=" + cats +
", map=" + map +
", info=" + info +
'}';
}
public Properties getInfo() {
return info;
}
public void setInfo(Properties info) {
this.info = info;
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
public List getCats() {
return cats;
}
public void setCats(List cats) {
this.cats = cats;
}
public String[] getFavorites() {
return favorites;
}
public void setFavorites(String[] favorites) {
this.favorites = favorites;
}
public User() {
}
public User(Integer id, String name, Integer age, Cat cat) {
this.id = id;
this.name = name;
this.age = age;
this.cat = cat;
}
public Cat getCat() {
return cat;
}
publi