一、工厂设计模式
1.1、传统的容器——EJB的缺点
EJB(Enterprise Java Beans),被称为企业Java Beans。他是上一代使用的容器。我们来看看传统的J2EE的体系。
EJB具有的缺点是很致命的:
- 运行环境苛刻。
- 代码移植性很差。
- EJB是重量级框架。
1.2、什么是Spring
Spring是一个轻量级的JavaEE解决方案,整合众多优秀的设计模式,其中最重要的设计模式是——工厂设计模式。他还包含其他的设计模式,比如说:代理设计模式、模板设计模式、策略设计模式等等。
1.3、什么是工厂设计模式
在传统的创建对象的时候,我们都是调用无参构造函数来创建对象的即new的方式来创建,这样创建对象的方式的耦合程度(指定是代码间的强关联关系,一方的改变会影响到另一方)就十分高。
一旦我们需要修改类型,就需要代码中修改,并且重新编译和部署。
1.4、工厂设计模式的实现
package com.factory; import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class BeanFactory { private static Properties env = new Properties(); static{ try { //第一步 获得IO输入流 InputStream inputStream = BeanFactory.class.getResourceAsStream("/applicationContext.properties"); //第二步 文件内容 封装 Properties集合中 key = userService ,value = com.service.impl.UserServiceImpl env.load(inputStream); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } /* 对象的创建的两种方式: 1. 直接调用构造方法创建对象 UserService userService = new UserServiceImpl(); 2. 通过反射的形式创建对象可以解耦合 Class clazz = Class.forName("com.service.impl.UserServiceImpl"); UserService userService = (UserService)clazz.newInstance(); */ public static UserService getUserService() { UserService userService = null; try { Class clazz = Class.forName(env.getProperty("userService")); userService = (UserService) clazz.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return userService; } public static UserDAO getUserDAO(){ UserDAO userDAO = null; try { Class clazz = Class.forName(env.getProperty("userDAO")); userDAO = (UserDAO) clazz.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return userDAO; } } 复制代码
userDAO = com.dao.userDAO userService = com.service.userService 复制代码
1.5、简单工厂的代码修改
我们可以发现,上面一个工厂设计模式的代码是又臭又长,我们每创建一个新的对象,都需要重新写一个工厂类,并且代码很大一部分都是相同的,仅仅只是创建的对象不同,于是我们可以抽取出共同的代码,组成一个通用的工厂类。
public class BeanFactory{ public static Object getBean(String key){ Object ret = null; try { Class clazz = Class.forName(env.getProperty(key)); ret = clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } return ret; } } 复制代码
1.6、总结
Spring本质上就是一个工厂,只不过我们在日常开发的时候使用的不是自己写的工厂,因为这个工厂的功能很少,性能很低下,Spring帮我们写好了一个大型工厂(ApplicationContext),我们只需要在一个固定的配置文(applicationContext.xml)中进行配置即可。
二、Spring入门
2.1、Spring简介
Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 是可以在 Java SE/EE 中使用的轻量级开源框架。
Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。
Spring 根据代码的功能特点,使用 Ioc 降低业务对象之间耦合度。IoC 使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring容器统一管理,自动“注入”,注入即赋值。 而 AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成“织入”。
2.2、Spring的优点
Spring 是一个框架,是一个半成品的软件。由 20 个模块组成。它是一个容器管理对象,容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。他的优点主要是以下几个方面。
- 轻量。
- 面向接口编程。
- 面向切面编程
- 可以轻易集成其他优秀的框架。
2.2.1、轻量
Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需的 jar 总共在 3M 左右。 Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar。
2.2.2、面向接口编程
Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。
2.2.3、面向切面编程(AOP)
通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付。 在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
2.2.4、集成其他优秀的框架
Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring提供了对各种优秀框架(如 Shiro、MyBatis)等的直接支持。简化框架的使用。 Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把这个插头放入插线板。不需要可以轻易的移除。
2.3、Spring的体系结构
Spring 由 20 多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、Web、面向切面编程(AOP, Aspects)、提供 JVM的代理(Instrumentation)、消息发送(Messaging)、核心容器(Core Container)和测试(Test)。
2.4、Spring的核心API
Spring的核心就是一个大工厂:ApplicationContext,他的作用是用于对象的创建,且可以解除耦合,他是一个接口,但是ApplicationContext是一个重量级的工厂对象占用大量的内存,所以我们不会频繁得去创建对象,一般一个应用只会创建一个工厂对象。ApplicationContext是线程安全的,可以被多线程并发访问。
他有两种实现方式:
- 适用于非WEB环境的:ClassPathXmlApplication
- 适用于WEB环境的:XmlApplicationContext
2.5、Spring的案例
2.5.1、引入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.5.RELEASE</version> </dependency> 复制代码
2.5.3、创建类型
package com.domain; /** * @author Xiao_Lin * @date 2021/2/4 15:57 */ public class Person { } 复制代码
2.5.4、修改配置文件
在applicationContext.xml的配置文件中更改配置
<!-- id属性:名字--> <!-- class属性:需要创建对象的全限定名--> <bean id="person" class="com.domain.Person"/> 复制代码
2.5.5、创建对象
/** * 用于测试Spring的第一个程序 */ @Test public void testSpring(){ // 1. 获得Spring的工厂 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml"); // 2. 通过工厂类来获得对象 Person person = (Person)applicationContext.getBean("person"); System.out.println(person); } 复制代码
2.6、细节分析
2.6.1、名词解释
Spring工厂创建的对象,叫做bean或者组件(componet)
2.6.2、相关方法
//通过这种方式获得对象,就不需要强制类型转换 Person person = ctx.getBean("person", Person.class); System.out.println("person = " + person); //当前Spring的配置文件中 只能有一个<bean>标签的class是Person类型 Person person = ctx.getBean(Person.class); System.out.println("person = " + person); //获取的是 Spring工厂配置文件中所有bean标签的id值 person person1 String[] beanDefinitionNames = ctx.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println("beanDefinitionName = " + beanDefinitionName); } //根据类型获得Spring配置文件中对应的id值 String[] beanNamesForType = ctx.getBeanNamesForType(Person.class); for (String id : beanNamesForType) { System.out.println("id = " + id); } //用于判断是否存在指定id值的bean if (ctx.containsBeanDefinition("a")) { System.out.println("true = " + true); }else{ System.out.println("false = " + false); } //用于判断是否存在指定id(name)值的bean if (ctx.containsBean("person")) { System.out.println("true = " + true); }else{ System.out.println("false = " + false); } 复制代码
2.7、Spring创建对象的简易原理图
注意:反射底层调用的是无参构造函数来进行实例化对象的,即使构造方法私有了,依然可以调用进行实例化对象。
2.8、注意
在未来开发的过程中,理论上所有的对象都是交给Spring工厂来创建,但是有一类特殊的对象——实体对象是不会交给Spring来创建的,它是交给持久层来创建的。
三、注入
3.1、什么是注入
注入是指 Spring 创建对象的过程中,将对象依赖属性通过配置设值给该对象。
3.2、为什么需要注入
通过编码的方式(setXxx),为成员变量进行赋值,存在耦合。
3.3、注入的方式
- set注入:其类必须提供对应 setter 方法。
- 构造器注入:利用构造器进行注入。
3.4、set注入
package com.domain; /** * @author Xiao_Lin * @date 2021/2/4 15:57 */ public class Person { private String username; private Integer password; @Override public String toString() { return "Person{" + "username='" + username + '\'' + ", password=" + password + '}'; } public Person(String username, Integer password) { this.username = username; this.password = password; } public Person() { } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Integer getPassword() { return password; } public void setPassword(Integer password) { this.password = password; } } 复制代码
<bean id="person" class="com.domain.Person"> <property name="username"> <value>Xiao_Lin</value> </property> <property name="password"> <value>123456</value> </property> </bean> 复制代码
/** * 用于测试注入 */ @Test public void testDI(){ ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml"); Person person = application.getBean("person", Person.class); System.out.println(person); } 复制代码
3.4.1、set注入的原理图
Spring通过底层调用对象属性对应的set方法完成对成员变量的赋值操作。
3.4.2、set注入详解
针对不同的不同类型的成员变量,我们不可能一直是使用value
标签,我们需要嵌套其他的标签,我们将成员变量可能的类型分类两大类:
- JDK内置类型。
- 用户自定义类型。
3.4.2.1、JDK内置类型
3.4.2.1.1、String+8种基本数据类型
都直接使用value
标签即可
<property name="password"> <value>123456</value> </property> 复制代码
3.4.2.1.2、数组类型
对于数组类型,我们需要在配置文件中,使用list
标签,表明是数组类型,嵌套value
标签来进行赋值。
@Data @AllArgsConstructor @NoArgsConstructor public class Person { private String[] emails; } 复制代码
<property name="emails"> <list> <value>124@qq.com</value> <value>456@163.com</value> </list> </property> 复制代码
3.4.2.1.3、Set集合
对于set集合类型,我们需要在配置文件中,使用set
标签,表明是set
集合类型,嵌套Set泛型中对应的标签来进行赋值。
@Data @AllArgsConstructor @NoArgsConstructor public class Person { private Set<String> tels; } 复制代码
<property name="tels"> <set> <value>123456</value> <value>456789</value> <value>13579</value> </set> </property> 复制代码
对于set集合由于我们规范了泛型为String,她是8种基本数据类型,所以在set标签中才嵌套value标签。如果没有规定泛型或者说是规定了其他的泛型,set嵌套的标签需要根据具体的情况来具体分析。
3.4.2.1.4、List集合
对于List集合类型,我们需要在配置文件中,使用list
标签,表明是List集合类型,嵌套List泛型中对应的标签来进行赋值。
list便签中嵌套什么标签,取决于List集合中的泛型。
@Data @AllArgsConstructor @NoArgsConstructor public class Person { private List<String> address; } 复制代码
<property name="address"> <list> <value>sz</value> <value>sz</value> <value>gz</value> </list> </property> 复制代码
3.4.2.1.5、Map集合
对于Map集合,有一个内部类——Entry,所以我们在配置文件中需要使用的标签是用map
标签来嵌套entry
标签,里面是封装了一对键值对。我们使用key
标签来表示键,里面嵌套键对应的标签,值要根据对应的类型来选择对应的标签。
@Data @AllArgsConstructor @NoArgsConstructor public class Person { private Map<String,String> qq; } 复制代码
<property name="qq"> <map> <entry> <key><value>zs</value></key> <value>123456</value> </entry> <entry> <key><value>lisi</value></key> <value>456789</value> </entry> </map> </property> 复制代码
3.4.2.1.6、Properties集合
Properties类似是特殊的Map,他的key
和value
都必须是String
类型。
在配置文件中,我们使用props
标签,里面嵌套prop
标签,一个prop
就是一个键值对,键写在key
属性中,值写在标签内部。
@Data @AllArgsConstructor @NoArgsConstructor public class Person { private Properties properties; } 复制代码
<property name="properties"> <props> <prop key="username">admin</prop> <prop key="password">123456</prop> </props> </property> 复制代码
3.4.2.2、自定义类型
3.4.2.2.1、第一种注入方式
@Data @AllArgsConstructor @NoArgsConstructor public class Hobby { private String name; } 复制代码
<property name="hobby"> <bean class="com.domain.Hobby"/> </property> 复制代码
我们可以发现第一种注入方式其实就是在property
标签里面写一个bean
标签,他的劣势也很明显:
- 配置文件代码冗余,我有一万个类需要引用同一个对象的时候,我同一段代码需要写一万次。
- 被注入的对象,被多次创建,浪费(JVM)内存资源,因为我每写一个
bean
标签意味着就创建一个新的对象。
3.4.2.2.2、第二种注入方式
鉴于第一种注入方式的缺点很明显,我们就需要改进,于是就有了第二种注入方式,这种方式是将我们需要注入的对象提前先创建一份出来,谁需要谁去引用即可。
<bean> <property name="hobby"> <ref bean="hobby"/> </property> </bean> <bean id="hobby" class="com.domain.Hobby"> <property name="name"> <value>admin</value> </property> </bean> 复制代码
3.4.3、set注入的简化写法
3.4.3.1、基于属性的简化
JDK类型注入
我们可以使用value
属性来简化value
标签的值,但是只可以简化8种基本数据类型➕Stirng类型的值。
<!--以前的方式--> <property name="name"> <value>Xiao_Lin</value> </property> <!--简化后的方式--> <property name="name" value="Xiao_Lin"/> 复制代码
用户自定义类型的注入
我们可以使用ref属性来简化ref
标签的值.
<!--以前的方式--> <property name="hobby"> <ref bean="hobby"/> </property> <!--简化后的方式--> <property name="hobby" ref="hobby"/> 复制代码
3.4.3.2、基于p命名空间的简化
我们可以发现,bean
标签的很多值都是重复且冗余的,于是可以使用p
命名空间来进行简化。
<!--内置数据类型--> <bean id="person" class="com.domain.Person" p:username="zs" p:password="123456" /> <!--用户自定义类型--> <bean id="hobbyBean" class="com.domain.Hobby"></bean> <bean id="hobby" class="com.domain.Person" p:hobby-ref="hobbyBean" 复制代码
3.5、构造注入
Spring调用构造方法,通过配置文件为成员变量赋值。如果要使用构造注入,必须提供有参的构造方法。
构造注入使用的标签是constructor-arg
标签,一个构造参数就是一对constructor-arg
标签。顺序和个数都必须和构造参数一样。
当出现构造方法重载的时候,我们可以通过控制constructor-arg
的个数来进行控制。如果出现构造参数个数相同的重载的时候(如第一个构造方法是给name赋值,第二个构造方法给type赋值),我们需要用type
属性来指定类型。
@ToString @AllArgsConstructor @NoArgsConstructor public class Hobby { private String name; private String type; } 复制代码
<bean id="hobbyBean" class="com.domain.Hobby"> <constructor-arg> <value>running</value> </constructor-arg> <constructor-arg> <value>dayily</value> </constructor-arg> </bean> 复制代码
/** * 用于测试构造注入 */ @Test public void testDI2(){ ClassPathXmlApplicationContext cxt = new ClassPathXmlApplicationContext("/applicationContext.xml"); Hobby hobbyBean = cxt.getBean("hobbyBean", Hobby.class); System.out.println(hobbyBean); } 复制代码
3.6、注入总结
四、控制反转(IOC)和依赖注入(DI)
4.1、控制反转(IOC)
控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值,依赖的管理。
简单来说控制反转就是把对于成员变量赋值的控制权,从代码中反转(转移)到Spring工厂和配置文件中完成。
IoC 是一个概念,是一种思想,其实现方式多种多样。Spring 框架使用依赖注入(DI)实现 IoC。
4.2、依赖注入(DI)
依赖:classA 类中含有 classB 的实例,在 classA 中调用 classB 的方法完成功能,即 classA对 classB 有依赖。
依赖注入(Dependency Injection):当一个类需要另一个类时,就可以把另一个类作为本类的成员变量,最终通过Spring的配置文件进行注入(赋值)。简单来说就是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。
Spring 的依赖注入对调用者与被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。
4.3、总结
Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称为 Bean。Spring 容器管理着容器中 Bean 之间的依赖关系,Spring 使用“依赖注入”的方式来管理 Bean 之间的依赖关系。使用 IoC 实现对象之间的解耦和。