一、工厂设计模式
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
属性来指定类型。
四、控制反转(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 实现对象之间的解耦和。
五、Spring工厂
5.1、简单对象和复杂对象
5.1.1、简单对象
简单对象指的就是可以直接通过调用构造方法(new)创建出来的对象。
5.1.2、复杂对象
复杂对象指的就是不可以直接通过调用构造方法(new)创建出来的对象。比如JDBC的Connection
对象、Mybatis的SqlSessionFactory
对象。
5.2、Spring创建复杂对象的三种方式
5.2.1、FactoryBean
5.2.1.1、FactoryBean接口
如果在applicationContext.xml
配置文件中配置的class
属性是FactoryBean
接口的实现类,那么通过id
属性获得的是这个类所创建的复杂对象(底层会调用重写的getObject()
方法)。
public class MyFactoryBean implements FactoryBean<Connection> {
// 用于书写创建复杂对象的代码,并且把复杂对象作为方法的返回值返回
@Override
public Connection getObject() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///javaweb?characterEncoding=utf-8&useSSL=false","root","123456");
return connection;
}
// 返回所创建的复杂对象的Class对象
@Override
public Class<?> getObjectType() {
return Connection.class;
}
// 配置是否是单例模式
@Override
public boolean isSingleton() {
return false;
}
<bean id="factoryBean" class="com.test.MyFactoryBean">
/**
* 用于测试factoryBean
*/
@Test
public void testFactoryBean(){
ClassPathXmlApplicationContext ctr = new ClassPathXmlApplicationContext("/applicationContext.xml");
Connection conn = (Connection) ctr.getBean("factoryBean");
System.out.println(conn);
}
5.2.1.2、FactoryBean接口的细节
- 如果我不想获得创建的复杂对象(Connection),想获得普通的简单对象(FactoryBean),我们仅仅只需在getBean(id)的前面加一个
&
即可。
import java.sql.Connection;
import java.sql.DriverManager;
import org.springframework.beans.factory.FactoryBean;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/24 19:47
*/
public class MyFactoryBean implements FactoryBean<Connection> {
@Override
public Connection getObject() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager
.getConnection("jdbc:mysql:///javaweb?characterEncoding=utf-8&useSSL=false", "root",
"1101121833");
return connection;
}
@Override
public Class<?> getObjectType() {
return Connection.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="factoryBean" class="MyFactoryBean">
</bean>
</beans>
import java.sql.Connection;
import org.junit.Test;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/24 19:50
*/
public class MyFactoryBeanTest {
/**
* 用于测试复杂类型对象的创建
*/
@Test
public void testMyFactoryBeanTest(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
MyFactoryBean connection = (MyFactoryBean)applicationContext.getBean("&factoryBean");// 获取普通的简单对象FactoryBean,不获取复杂的Connection对象
System.out.println(connection);
}
}
- isSingleton()方法,如果返回值为true时,他只会创建一个对象,返回false时会创建多个对象,一般根据对象的特点来判断返回true(SqlSessionFactory)还是false(Connection)。
5.2.1.3、BeanFactory实现原理图
5.2.1.4、FactoryBean总结
FactoryBean是Spring中用于创建复杂对象的一种方式 也是Spring原生提供的,后面框架整合会大量运用。
5.2.2、实例工厂
5.2.2.1、FactoryBean的弊端
使用FactoryBean的话有Spring的侵入,实现了FactoryBean接口,一旦离开了Spring,整个类都无法使用。
5.2.2.2、实例工厂的使用
// 实例工厂
public class ConnectionFactory {
public Connection getConnection(){
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql:///javaweb?characterEncoding=utf-8&useSSL=false","root","1101121833");
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
}
<bean id="connFactory" class="com.factory.ConnectionFactory"/>
<bean id="conn" factory-bean="connFactory" factory-method="getConnection"/>
5.2.3、静态工厂
前面我们学了实例工厂,由于实例工厂的getConnection()
方法是实例方法,需要由对象来调用,所以需要先创建对象然后再通过对象来调用方法。
而静态工厂由于getConnection()
方法是静态方法,不需要由对象来调用,直接通过类进行调用。这就是实例工厂与静态工厂最大的区别。
public class ConnectionStaticBeanFactory {
public static Connection getConnection(){
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql:///javaweb?characterEncoding=utf-8&useSSL=false","root","1101121833");
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
}
<bean id="staticBeanFactory" class="com.factory.ConnectionStaticBeanFactory" factory-method="getConnection"/>
5.3、创建对象的细节
5.3.1、控制简单对象的创建次数
控制简单对象的创建次数我们只需要配置bean
标签的scope
属性值即可。他常用的有两个值:
- singleton:默认为单例模式,只会创建一个简单对象。
- prototype:每次都会创建一个新的对象。
<bean id="person" scope="singleton(prototype)" class="com.doamin.Person"/>
5.3.2、控制复杂对象的创建次数
FactoryBean接口的isSingleton()
方法的返回值来进行控制(如果没有isSingleton()
方法,那么还是通过scope
属性来进行控制):
- 返回true:只会创建一次。
- 返回false:每一次都会创建一个新的对象。
5.3.3、控制对象创建次数的原因
可以被大家共享的对象(SqlSessionFactory、各种Dao、Service)可以只创建一次,不可以被大家共享的对象(Connection、SqlSession、Controller)可以创建多次,控制对象创建次数的最大好处是可以节省不必要的内存浪费。
5.4、对象的生命周期
生命周期指的是一个对象的创建、存活、消亡的一个完整过程。由Spring来负责对象的创建、存活、销毁。了解生命周期,有利于我们使用好Spring为我们创建的对象。
Spring帮我们创建的对象有三个阶段:
- 创建阶段
- 初始化阶段
- 销毁阶段
5.4.1、创建阶段
- 当 scope = “singleton” 时,Spring工厂创建的同时,对象会随之创建。如果我们不想在Spring工厂创建的同时创建,想在获取对象的时候创建,只需在配置文件的
bean
标签添加一个lazy-init = true
即可。 - 当 scope = “prototype” 时,Spring工厂会在获取对象的同时创建对象。
5.4.2、初始化阶段
Spring工厂在创建完对象后,会调用对象的初始化方法,完成对应的初始化操作。
初始化方法是由程序员根据需求提供初始化方法,由Spring工厂调用,最终完成初始化操作。他有两种调用的方式:
- 实现InitializingBean接口(有Spring侵入的问题)。
- 提供一个普通方法并修改配置文件。
5.4.2.1、InitializingBean接口
// 这个就是初始化方法,做一些初始化操作,Spring会进行调用
@Override
public void afterPropertiesSet() throws Exception {
// 初始化操作
}
5.4.2.2、提供普通方法
由于实现InitializingBean接口存在Spring侵入的问题,所以Spring提供了另一个方法给我们进行初始化操作,那就是提供一个普通的方法,然后去配置文件中增加init-method="方法名"
熟悉的配置即可。
public void init(){
System.out.println("我是初始化方法");
}
<bean id="product" class="com.domain.Product" init-method="init"/>
5.4.2.3、注意
如果一个对象既实现了InitializingBean接口同时又提供了普通的初始化方法,那么两个初始化方法都会执行,先执行的是InitializingBean
接口的方法,再执行普通的初始化方法。
在执行初始化操作之前,会先进行属性的注入,注入在前,初始化在后。
初始化需要做的操作一般是数据库、IO、网络操作。
5.4.3、销毁阶段
在工厂关闭之前,Spring会在销毁对象前,会调用对象的销毁方法,完成销毁操作。
销毁方法是程序员根据需求定义销毁方法,由Spring工厂调用销毁方法,完成销毁操作。他也有两种方法:
- 实现DisposableBean接口。
- 定义普通的销毁方法在配置文件中配置。
5.4.3.1、实现DisposableBean接口
public class Product implements InitializingBean, DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("销毁操作,资源释放");
}
}
5.4.3.2、定义普通方法
public class Product implements InitializingBean, DisposableBean {
public void MyDestory(){
System.out.println("自己定义的销毁方法");
}
}
<bean id="product" class="com.domain.Product" destroy-method="MyDestory"/>
5.4.3.3、注意
- 销毁方法的操作只适用于scope=“singleton”。
- 销毁操作主要指的是一些资源的释放操作。
5.5、Spring整合配置文件
一般来说像数据库的一些配置信息我们都不会直接写在代码里面,会将他们抽取出来成一个配置文件,再利用Spring进行注入。我们只需要加入一个标签即可完成。
<!--告诉Spring你的db.properties在哪里-->
<context:property-placeholder location="classpath:/db.properties"/>
<!--用$(db.properties中的key)来进行取值-->
<bean id="conn" class="com.factory.BeanFactory">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
六、自定义类型转换器
6.1、类型转换器
我们写在Spring配置文件中赋值的值都是String类型的,但是我们的实体类是Interger类型的值,按照语法来说,String类型的值是不可以直接赋值给Integer类型的,但是为什么能直接赋值呢?
因为Spring内部帮我们进行了自动的类型转换,Spring通过类型转换器将配置文件中字符串类型的数据,转换成了对象中成员变量对应类型的数据,从而完成注入。
6.2、自定义类型转换器
6.2.1、问题引入
@Data
@AllArgsConstructor
@NoArgsConstructor
public class People {
private String name;
private Date birthday;
}
<bean id="people" class="com.domain.People">
<property name="name" value="XiaoLin"/>
<property name="birthday" value="2021-2-6"/>
</bean>
我们运行代码之后发现报错了,说String类型的值不可以转化为Date类型的值,说明Spring内部没有这个转换器。
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Date' for property 'birthday': no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:262)
at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:585)
... 39 more
Spring内部没有提供特定类型转换器时,而程序员在应用的过程中又需要使用,所以需要程序员自己定义类型转换器。
6.2.2、代码实现
自定义类型转换器我们分为两步实现:
- 实现
Converter<转换前的类型, 转换后的类型>
接口,并且重写里面的方法。 - 在配置文件中进行转换器的注册
public class MyConverter implements Converter<String, Date> {// 他有两个泛型,一个是转换前的类型,另一个是转换后的类型
/*
convert方法的作用是将String->Date
parm:source代表的是配置文件中需要转换的内容
return:把转换好的值作为返回值,Spring会自动为属性赋值
*/
@Override
public Date convert(String source) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date parse = null;
try {
parse = sdf.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return parse;
}
}
<!-- 类型转换器的注册,告诉Spring我们所创建的MyConverter类是类型转换器类,
Spring提供了一个类ConversionServiceFactoryBean来完成类型转换器的注册-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters" >
<set>
<!-- 注册类型转换器-->
<ref bean="myConvert"/>
</set>
</property>
</bean>
6.2.3、注意细节
- 创建
ConversionServiceFactoryBean
标签的id
必须为conversionService
,不然不生效。 - 其实Spring已经内置了日期类型的转换器,但是他只支持以
/
作为分隔符的字符串的格式:2021/2/6,不支持其他的格式,如果你的字符串格式已经是这种,就无需再写自定义类型转换器。
七、BeanPostProcessor
7.1、概述
BeanPostProcessor称为后置处理Bean,他的作用是对Spring工厂所创建的对象进行二次加工,他是AOP的底层实现,他本质上是一个接口。
7.2、BeanPostProcessor分析
他是一个接口,要实现他的两个方法:
Object postProcessBeforeInitialization(Object bean,String beanName)
:他的作用是在Spring创建完对象后,在进行初始化方法之前,- 通过参数获取到Spring创建好的对象,执行
postProcessBeforeInitialization
方法进行加工,最终通过返回值返回这个加工好的对象给Spring。 Object postProcessAfterInitialization(Object bean,Stirng beanName)
:Spring执行完对象的初始化操作之后,运行postProcessAfterInitialization
方法进行加工,通过参数获取Spring创建好的对象,最终通过返回值返回给Spring。
在日常的开发中,我们很少去处理Spring的初始化操作,所以没有必要区分前后,所以一般只需要实现其中一个方法即可,且BeanPostProcessor会对Spring工厂中的所有对象进行加工。
7.3、代码实现
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private Integer id;
private String name;
}
public class MyBeanPoster implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 要进行类型判断,如果不是Student类型的话直接返回,不然会报类型转换错误,因为Spring会把工厂中的所有对象进行加工处理
if (bean instanceof Student){
Student student = (Student) bean;
student.setName("lisi");
}
return bean;
}
}
<bean id="student" class="com.beanpost.Student">
<property name="id" value="10"/>
<property name="name" value="zs"/>
</bean>
<!--注册后置Bean-->
<bean id="myBeanProcessor" class="com.beanpost.MyBeanPoster"/>
八、AOP编程
8.1、静态代理
8.1.1、问题引入
为什么需要代理设计模式
在JavaEE分层开发中,Service层(业务层)对我们来说是最重要的。
Service层中包含哪些代码
在Service中会出现两种类型的代码:
- 核心功能:业务运算、Dao操作。
- 附加功能(代码量少且不属于核心功能,可有可无):事务、日志、性能监控。
8.1.2、代理设计模式概述
目标类(原始类):类似于现实生活中的房东,指的是包含核心功能的业务类。
目标方法(原始方法):目标类(原始类)中的方法。
通过代理类,为原始类(目标类)增加额外的功能,好处是有利于原始类(目标类)的维护。
8.1.3、静态代理的实现
静态代理有一个原始类就必须有一个手工编写的代理类(源代码),每一个类都是程序员手动写的。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String username;
private String password;
}
public interface UserService {
void register(User user);
void login(String username,String password);
}
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("注册了");
}
@Override
public void login(String username, String password) {
System.out.println("登录了");
}
}
public class UserServiceProxy implements UserService {
// 原始类对象
UserServiceImpl userService = new UserServiceImpl();
@Override
public void register(User user) {
System.out.println("增加了日志的额外功能");
userService.register(user);
}
@Override
public void login(String username, String password) {
System.out.println("增加了登录日志的额外功能");
userService.login(username,password);
}
}
8.1.4、静态代理存在的问题
- 一个原始类就有一个手动书写的代理类,会导致静态类的文件过多,不利于项目管理。
- 额外功能的维护性差,修改复杂。
8.2、Spring动态代理
既然静态代理如此的复杂和麻烦,那么Spring就帮我们搞了一个动态代理,简化了我们的开发。
8.2.1、动态代理的实现
- 创建原始对象(目标对象)
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("注册了");
}
@Override
public void login(String username, String password) {
System.out.println("登录了");
}
}
<bean id="UserServiceImpl" class="com.proxy.dynamicProxy.UserServiceImpl"/>
- 书写额外功能
既然我们不需要手动书写代理类了,那么如何告诉Spring我们需要增加的额外方法呢?
我们需要写一个额外方法类,并且实现MethodBeforeAdvice
接口,在他规定的方法中书写额外功能
public class Before implements MethodBeforeAdvice {
// 额外功能书写在接口的实现中,运行在原始方法执行之前运行额外功能
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("---method before advice log---");
}
}
- 去配置文件中配置
<bean id="before" class="com.proxy.dynamicProxy.Before"/>
- 定义切入点
切入点:额外功能加入的位置,由程序员根据自己的需求,决定额外功能加入给哪个原始方法。
在测试阶段,所有方法都作为切入点,都加入额外功能。
<aop:config>
<!-- 切入点,id属性是唯一标识符,expression属性是切入点表达式,这个切入点表达式的意思是所有方法都作为切入点都加入额外功能-->
<aop:pointcut id="testaop" expression="execution(* *(..))"/>
</aop:config>
- 组装
组装的目的是将切入点和额外功能进行整合。
<!-- 组装:将切入点和额外功能进行整合-->
<aop:advisor advice-ref="before" pointcut-ref="testaop"/>
- 测试
利用原始对象的id
值,可以获取由Spring工厂创建的代理对象。获得代理对象后,可以声明接口类型进行对象的存储。
8.2.2、MethodBeforeAdvice
如果我们想实现动态代理,额外功能必须实现MethodBeforeAdvice
接口的before
方法,before
方法的参数解释
参数名 | 含义 |
---|---|
Method method | 额外功能所增加给的那个原始方法,给谁增加额外功能就是谁,这个参数是变化的,取决于给谁增加额外方法 |
Object[] objects | 额外功能所增加给的那个原始方法的参数。给login(String uername,Stirng password)方法增加额外功能,那么这个Object数组就是对于login方法的参数列表,和上一个参数息息相关。 |
Object o | 额外功能所增加给的那个原始对象。 |
8.2.3、注意事项
-
Spring创建的动态代理类在哪里?
Spring框架在运行时,通过动态字节码技术,在JVM里创建,等待程序结束后,会和JVM一起消失。
-
什么是动态字节码技术?
通过第三方动态字节码框架,直接生成JVM生成字节码,进而创建对象,当JVM结束时,动态字节码跟着消失。
-
动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理类文件数量过多、影响项目管理的问题。
-
在不改变功能的前提下,创建其他目标类(原始类)的代理对象时,只需要指定原始(目标)对象即可。
8.2.4、MethodInterceptor接口
MethodBeforeAdvice
接口的方法作用比较单一,仅仅只是可以在原始方法执行之前进行增加额外功能,Spring还提供了另一接口——MethodInterceptor
接口,他不仅仅可以在原始方法执行之前增加额外功能,还可以在原始方法执行之后增加额外功能,甚至执行前后都可以增加。
public class Arround implements MethodInterceptor {
/*
书写额外功能的方法
参数:MethodInvocation表示的是额外功能所增加给的那个原始方法。
运行原始方法:methodInvocation.proceed(),在原始方法前面写的代码就运行在原始方法之前,反之。
返回值:代表原始方法返回值
*/
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("前置增强");
Object proceed = methodInvocation.proceed();
System.out.println("后置增强");
return proceed;
}
}
8.2.5、切入点
切入点决定额外功能加入的位置。他分为两部分:
execution()
:切入点函数* *(..)
:切入点表达式
8.2.5.1、方法切入点表达式
* *(..)
:第一*
对应方法的修饰符(*
表示任意),第二个*
对应方法的方法名,(..)
对应方法的任意参数列表,所以这个切入点表达式表示的是所有方法。
定义login方法作为切入点
* login(..)
定义login方法且方法有两个字符串类型的参数作为切入点
* login(String,String)
这个方式有一个很致命的缺陷:切入的方法不够精准。我们需要使用精准方法切入点限定。所以我们在指定方法的时候,如果需要精准一点,需要指定包名+类名。
8.2.5.2、类切入点表达式
指定特定的类作为切入点(额外功能加入的位置),这个类中的所有方法都会加上对应的额外功能。
* com.domain.UserService.*(..)
8.2.5.3、包切入点表达式
指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能。在实战中运用比较多。
# 切入点包中的所有类,必须在proxy中,不能在proxy包的子包中
* com.domain.proxy.*.*(..)
# 如果想要当前包及其当前包的子包都进行功能增强的话,必须要这样写
* com.domain.proxy..*.*(..)
8.2.5.4、切入点函数
切入点函数式用于执行切入点表达式,execution
是最为重要的切入点函数,功能最全,可以执行方法切入点表达式、类切入点表达式、包切入点表达式。
他的弊端是执行切入点表达式时,书写比较麻烦。所以Spring提供了其他切入点函数来进行简化execution
书写的复杂度。
8.2.5.4.1、args
他的主要作用是用于函数(方法)的参数匹配。
# 方法参数必须是两个字符串类型的参数
args(String,String)
8.2.5.4.2、within
主要用于类、包切入点表达式的匹配。
# 切入点想选为某个类(UserServiceImpl这个类作为切入点)
whithin(*..UserServiceImpl)
# 切入点想选为某个包
within(com.poroxy..*)
8.2.5.4.3、@annotation
为具有特殊注解的方法加入额外功能,语法格式:@annotation(注解所在的包的全限定名)
// 先写一个自定义注解
@Target(ElementType.METHOD) // 表示可以加在哪里
@Retention(RetentionPolicy.RUNTIME) // 表示什么时候起作用
public @interface Log {
}
<aop:pointcut id="testaop" expression="@annotation(com.anno.Log)"/>
@Log
@Override
public void register(User user) {
System.out.println("注册了");
}
8.2.5.5、切入点函数的逻辑运算
切入点函数的逻辑运算指的是整合多个切入点函数一起配合工作,可以完成更加复杂的需求。
8.2.5.5.1、与操作(and)
# 案例一:满足方法名为login且参数为两个字符串
execution (* login(..) and args(String,String))
注意:与操作不能用于同种类型的切入点函数。
# 案例二:满足方法名为login和register作为切入点
# 这是错误的,不可能一个方法同时叫login和register
execution (* login(..)) and execution(* register(..))
8.2.5.5.2、或操作(or)
# 案例一:满足方法名为login或register作为切入点
execution(* login(..)) or execution(* register(..))
8.2.6、总结
8.3、AOP概述
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。切面=切入点+额外功能。
AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
他的作用是在程序运行期间,在不修改源码的情况下对方法进行功能增强,优势是可以减少重复代码,提高开发效率,并且便于维护。
8.4、名词解释
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
- Target(目标对象):代理的目标对象。
- Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
- Pointcut(切入点):所谓切入点是指我们要对哪些 。
- Joinpoint:进行拦截的定义。
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
- Aspect(切面):是切入点和通知(引介)的结合,简单来说就是切入点+增强方法。
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
8.5、AOP底层实现(动态代理)
8.5.1、JDK的动态代理
public class TestJDKProxy {
public static void main(String[] args) {
// 1. 创建原始对象
UserService userService = new UserServiceImpl();
// 2. 创建JDK动态代理
InvocationHandler handler = new InvocationHandler() {
@Override
// 参数一:表示代理对象 参数二:额外功能所增加给的原始方法 参数三: 表示原始方法的参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 原始对象的方法方法运行
Object ret = method.invoke(userService,args);
System.out.println("========after proxy log========");
return ret;
}
};
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),handler);
userServiceProxy.login("admin","123456");
userServiceProxy.register(null);
}
}
8.5.2、CGlib的动态代理
JDK的动态代理是通过实现接口从而保证代理类和原始类的方法一致,但是如果碰到没有接口的时候呢,那么就需要使用到CGlib的动态代理了,他和JDK的动态代理最大的区别是CGlib的动态代理是通过父子继承的手段来实现代理类和原始类方法一致的。
CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法的一致,同时在代理类中也可以做新的实现。
package cn.linstudy.cglibProxy.service;
import cn.linstudy.cglibProxy.domain.User;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/25 18:44
*/
public interface UserServiceImpl {
void login(String username,String password);
void register(User user);
}
package cn.linstudy.cglibProxy.proxy;
import cn.linstudy.cglibProxy.domain.User;
import cn.linstudy.jdkProxy.service.impl.UserServiceImpl;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/25 18:45
*/
public class UserServiceProxy extends UserServiceImpl {
@Override
public void login(String username, String password) {
System.out.println("登录了"+username+password);
}
@Override
public void register(User user) {
System.out.println("注册了"+user);
}
}
public class TestCGlib {
public static void main(String[] args) {
// 创建原始对象
UserService userService = new UserService();
// 通过CGlib方式创建代理对象
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(userService.getClass().getClassLoader()); // 设置类加载器
enhancer.setSuperclass(userService.getClass()); // 设置父类
MethodInterceptor interceptor = new MethodInterceptor() {
// 等同于InvocationHandler 的 invoke 方法
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("=====cglib log=====");
Object ret = method.invoke(userService, args);
return ret;
}
};
enhancer.setCallback(interceptor);
UserService serviceProxy = (UserService) enhancer.create();
serviceProxy.login();
serviceProxy.register();
}
}
8.5.3、总结
- JDK动态代理:Proxy.newProxyInstance(),通过接口创建代理实现类。
- CGlib动态代理:Enhancer,通过继承父类的方式创建代理类。
8.6、用注解实现AOP
public interface UserService {
void register(User user);
void login(String username, String password);
}
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("注册了");
}
@Override
public void login(String username, String password) {
System.out.println("登录了");
}
}
@Aspect
public class MyAspect {
@Around("execution(* * (..))") // 写切入点表达式
// joinPoint 表示原始方法
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("====aspect前置增强====");
Object ret = joinPoint.proceed(); // 代表原始方法执行
System.out.println("====aspect后置增强====");
return ret;
}
}
8.6.1、@Pointcut
如果我们想为多个方法配置同一个切入点表达式,那么就会出现冗余,这个时候我们能想到的就是将公共的切入点表达式提取出来,那么就需要使用到一个注解:@Pointcut
,注意的是,注解所在的方法必须是public void修饰,且没有方法体。
切入点复用就是在切面类中定义一个函数,用@Pointcut
注解,通过这种方式,定义切入点表达式,后续更加有利于切入点的复用。
@Aspect
public class MyAspect {
// 将公共的切入点表达式提取出来,注意的是,注解所在的方法必须是public void修饰,且没有方法体
@Pointcut("execution(* * (..))")
@Around(value = "MyPoint()") // 引入切入点表达式
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { // joinPoint 表示原始方法
System.out.println("====aspect前置增强====");
Object ret = joinPoint.proceed(); // 代表原始方法执行
System.out.println("====aspect后置增强====");
return ret;
}
@Around(value = "MyPoint()") // 引入切入点表达式
public Object Around1(ProceedingJoinPoint joinPoint) throws Throwable { // joinPoint 表示原始方法
System.out.println("====aspect tx====");
Object ret = joinPoint.proceed(); // 代表原始方法执行
System.out.println("====aspect tx====");
return ret;
}
}
8.6.2、动态代理的创建方式
AOP底层实现有两种:
- JDK的动态代理:通过接口实现,做新的实现方法来创建代理对象。
- CGlib动态代理:通过继承父类,做一个新的子类出来创建代理对象。
在默认情况下,我们dubug一下可以发现,默认使用的是JDK动态代理的方式来进行AOP编程的。
在某些情况下,我们想将默认的JDK动态代理的方式转变为CGlib动态代理的方式,那么该如何实现呢?
在配置文件中,我们之前写过一个配置<aop:aspectj-autoproxy />
,这段配置的作用是告诉Spring,我们要开始基于注解在进行AOP编程了,这段配置中有一个属性proxy-target-class="false"
,他的默认值是false
,表示默认使用JDK的动态代理,如果将中国值设置为true
,那么则表示使用CGlib动态代理。
这个标签只适用于基于注解的AOP开发。如果是基于传统的AOP开发的话,不基于注解,那么需要在<aop-config>
标签中,添加proxy-target-class="false"
属性配置即可。和注解的方式相比,属性是一样的,只是写的位置不一样。
8.7、总结
九、注解编程
9.1、注解编程概述
注解编程指的是在类或者方法上加入特定的注解,完成特定功能的开发。使用注解的好处:
- 注解编程开发更方便,代码简洁,开发速度大大提升。
- 注解编程是Spring开发的新趋势。
9.2、注解的作用
-
简化xml文件中繁杂的配置。
-
替换接口,实现调用双方的契约性。通过注解的方式,在功能调用者和功能提供者之间达成约定,进而进行功能的调用。注解形式是主流。
9.3、对象创建相关的注解
这个阶段的注解仅仅只是为了简化XML配置而存在的,并不能完全替代XML。
9.3.1、@Component
9.3.1.1、代码示例
既然我们开始使用注解开发,那么就需要告诉Spring,让Spring框架在你设置的包及其子包中扫描对应的注解,让他生效。
<context:component-scan base-package:"com.lin" />
package cn.lin;
import org.springframework.stereotype.Component;
@Component
@Data
public class User {
private String id;
private String username;
private String password;
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="cn.lin"/>
</beans>
package cn.lin;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/24 21:58
*/
public class UserTest {
/**
* 用于测试注解@Component
*/
@Test
public void testComponent(){
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = app.getBean("user", User.class);
System.out.println(user);
}
}
9.3.1.2、注意
- 使用
@Component
注解,他创建的对象的id值默认是类名的首字母小写。 - 我们可以通过
@Component("自定义id值")
方式来给创建的对象指定一个id值。 - Spring配置文件是可以覆盖注解配置的内容。id的值和class的值需要保持一致,如果不一致的话Spring会以为这个是一个新的对象,进而不去覆盖。
9.3.1.3、@Component的衍生注解
@Component有三个衍生注解:
- @Repository
- @Service
- @Controller
这些注解本质上就是@Component注解,用法(替代标签)和细节与@Component没有任何区别,本质上是一模一样的。我们查看源码@Repository可以证明这个结论(其他同理可证)。
Spring提供这些注解是为了更准确的表达一个类型的作用。
- @Repository:主要用于Dao接口。
- @Service:主要用于Service实现类。
- @Controller:主要用于控制器。
9.3.2、@Scope
9.3.2.1、概述
@Scope
注解用于控制简单对象的创建次数。如果我们不添加@Scope
注解的话,他有一个默认值是singleton
。
9.3.2.2、代码示例
// 单例模式,只会创建一次
@Scope("singleton")
public class User(){
}
9.3.3、@Lazy
9.3.3.1、概述
@Lazy
注解的作用是延迟创建单实例对象。
如果没有进行配置的话,默认是在容器创建的时候,同步创建对象。
如果添加了注解的话,在容器创建的时候就不会同步创建对象,而是在你需要使用的时候再创建对象。
9.4、声明周期方法相关的注解
9.4.1、@PostConstruct
@PostConstruct
注解用于方法上,表示这个方法是一个初始化方法。在引入这个注解时,我们需要先加入一组依赖。
<!-- https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@Component
@Data
public class User {
// 表示这个是一个初始化方法
@PostConstruct
public void MyInit(){
System.out.printf("User Init");
}
9.4.2、@PreDestroy
@PreDestroy
注解用于方法上,表示这个方法是一个销毁方法。工厂关闭时会自动调用销毁方法。
@PreDestroy
public void MyDestory(){
System.out.printf("User Destory");
}
9.4.3、注意
这连个注解都不是Spring提供的,是JSR(JavaEE规范)520提供的。所以需要导入依赖。
9.5、注入相关的注解
9.5.1、用户自定义类型:@Autowired
9.5.1.1、概述
@Autowired
是一种注解,可以对成员变量、方法和构造函数进行标注,来完成自动装配的工作.
@Autowired
标注可以放在成员变量上,也可以放在成员变量的set方法上,也可以放在任意方法上表示,自动执行当前方法,如果方法有参数,会在IOC容器中自动寻找同类型参数为其传值。
9.5.1.2、代码示例
package cn.lin.dao;
import cn.lin.User;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/26 9:17
*/
public interface UserMapper {
public void login(User user);
}
package cn.lin.dao;
import cn.lin.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/26 9:18
*/
@Repository
public class UserMapperImpl implements UserMapper{
@Override
public void login(User user) {
System.out.printf("登录了");
}
}
package cn.lin.service;
import cn.lin.User;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/26 9:19
*/
public interface UserService {
void login(User user);
}
package cn.lin.service.impl;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/26 9:19
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void login(User user) {
userMapper.login(new User());
}
}
/**
* 用于测试
*/
@Test
public void test(){
ApplicationContext app = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService)app.getBean("userServiceImpl");
userService.login(new User());
}
9.5.1.3、注意
-
@Autowired
注解默认是基于类型进行注入,注入对象的类型必须与目标成员变量类型相同或者是其子类、实现类。 -
要是想基于名字进行注入的话,需要
@Autowired
结合另一个注解:@Qualifier("需要注入的类的id值")
。基于名字的注入要求注入的对象的ud值必须与@Qualifier
注解中设置的名字相同。 -
@Autowired
注解可以放置的位置:- 可以放置在对应成员变量的set方法上。Spring变量会调用set方法进行注入。
- 直接放置在需要注入的成员变量之上。Spring会通过反射直接对成员变量进行注入。(推荐)
十、Spring整合Mybatis
10.1、整合概述
JavaEE开发需要持久层来进行访问数据库的操作,但是现有的持久层开发过程中存在大量的代码冗余,不方便我们开发和后期维护,而Spring基于模板设计模似对于上述的持久层技术进行了封装,方便了我们的开发和减少了代码冗余。
10.2、传统Mybatis编码存在的问题
MyBatis开发步骤回顾:
- 编写实体类。
- 配置实体别名。
- 创建表。
- 创建DAO接口。
- 实现Mapper文件。
- 注册Mapper文件。
- MyBatis的API的调用。
我们在开发MyBatis的时候可以发现,传统的MyBatis的开发有一个很致命的弊端:配置繁琐且代码冗余。于是我们需要Spring这个超级工厂来进行整合。
10.3、整合思路
10.4、代码实现
MyBatis整合Spring的重要改变就是改变了配置文件以及测试类的代码。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///javaweb?characterEncoding=utf-8&useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1101121833"/>
</bean>
<!-- 创建SqlSessionFactory、创建SqlSessionFactoryBean-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 数据源-->
<property name="dataSource" ref="dataSource"/>
<!-- 别名-->
<property name="typeAliasesPackage" value="com.lin.domain"/>
<!-- mapper配置文件路径-->
<property name="mapperLocations">
<list>
<value>classpath:com/lin/mapper/*Mapper.xml</value>
</list>
</property>
</bean>
<!-- Dao对象的创建-->
<bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- sqlSessionFactoryBean对象-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<!-- Dao接口的位置-->
<property name="basePackage" value="com.lin.mapper"/>
</bean>
</beans>
/**
* 用于测试Spring与Mybatis整合
*/
@Test
public void testSpringMybatis(){
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext(
"/applicationContext.xml");
UserMapper userMapper = (UserMapper) app.getBean("userMapper");
List<User> users = userMapper.queryAll();
System.out.println(users);
}
10.5、注意
- Spring与Mybatis整合的时候,为什么不提交事物,但是我们插入数据的时候依然可以插入到数据可呢?
分析:本质上谁控制了链接对象,谁就可以提交事物。连接对象是由连接池来获取的:
1. MyBatis提供的连接池对象:他将Connection.setAutoCommit的值从默认值true设置成为了false,所以导致无法自动提交,手动控制事物,需要在操作完成后,手工提交事务。
2. Druid(C3P0、DBCP)提供的连接池对象:Connection.setAutoCommit的值为默认值true,保持自动控制事物,自动提交。
- 在未来实际开发中,一般都是多条sql一起成功或者一起失败,我们还是需要手动提交。但是后续我们会把事物交给Spring通过事物控制来解决。
10.6、Spring的事物处理
10.6.1、事物概述
保证业务操作完整性的一种数据库机制。具有四个特性:A(原子性)、C(一致性)、I(隔离性)、D(持久性)。
常见的两种持久化技术的事物控制:
-
JDBC (依赖于Connection对象来进行事物控制)
Connection.setAutoCommit(false);
Connection.commit();
Connection.rollback();
-
MyBatis(自动提交事务)
sqlSession.commit();
sqlSession.rollback();
-
结论:控制事物的底层都是Connection对象来控制的(sqlSession的底层也是Connection)。
9.6.2、编码实现
<!-- 配置原始对象-->
<bean id="userService" class="com.lin.service.impl.UserServiceImpl">
<property name="userMapper" ref="userMapper"/>
</bean>
<!-- 额外功能,Spring帮我们封装好了一个对象DataSourceTransactionManager,简化我们的开发-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 先注入数据源,获取链接对象,进而才可以控制事物-->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 组装切面-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
@Transactional // 切入点,给谁加入事物
public class UserServiceImpl implements UserService {
private UserMapper userMapper;
public UserMapper getUserMapper() {
return userMapper;
}
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public List<User> queryAll() {
return userMapper.queryAll();
}
}
十一、事务
11.1、事务的属性
属性是描述物体特征的一系列值,事务有五个属性:
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
我们可以用@Transactional()
注解在添加属性,在括号被给事务添加属性。
11.2、隔离属性
隔离属性描述了事务解决并发问题的特征。事务并发访问会产生三个问题:
- 脏读
- 不可重复读、
- 幻读
我们可以通过隔离属性来解决事务并发产生的问题,在隔离属性中设置不同的值。
11.2.1、脏读
一个事务读取了另一个事务中没有提交的数据,会在本事务中产生数据不一致的问题。这个是Oracle的默认属性。
解决方案:给@Transactional()
注解添加isolation=Isolation.READ_COMMITTED
。
11.2.2、不可重复读
一个事务中,多次读取相同的数据,但是读取的结果不一样,会在本事务中产生数据不一致的问题。他读取的数据不是脏数据,只是查询的数据一致。这个是MySQL的默认属性。
解决办法:给@Transactional()
注解添加isolation=Isolation.REPEATABLE.READ
。他的本质是给该条数据加一把行级锁。
11.2.3、幻读
一个事务,多次对整表进行查询统计,但是结果的行数不一样,会在本事务中产生数据不一致的问题。
解决办法:给@Transactional()
注解添加isolation=Isolation.SERIALIZABLE
。他的本质是给整个表加一把表锁。
11.2.4、总结
并发安全排序:Isolation.SERIALIZABLE>Isolation.REPEATABLE.READ>Isolation.READ_COMMITTED
性能排序:Isolation.SERIALIZABLE<Isolation.REPEATABLE.READ<Isolation.READ_COMMITTED
# 我们可以用命令来查询MySQL的默认属性。
select @@tx_isolation
隔离属性的值 | MySQL | Oracle |
---|---|---|
ISOLATION_READ_COMMITTED | ✅ | ✅ |
IOSLATION_REPEATABLE_READ | ✅ | ❎ |
ISOLATION_SERIALIZABLE | ✅ | ✅ |
11.3、传播属性
传播属性描述了事务解决嵌套问题的特征。
事务嵌套:一个大的事务里面包含着几个小的事务。
事务嵌套由于大事务中融入了很多的小事务,他们彼此影响,最终就会导致外部的大事务丧失了事务的原子性。我们用传播属性的值来解决这个问题,传播属性的值保证了在同一时间只会有一个事务存在。
11.3.1、事务传播属性详解
融合:自己的事务没有了,以外部的事务为准。
挂起:相当于暂停。
传播属性的值 | 外部不存在事务 | 外部存在事务 | 用法 | 使用场景 |
---|---|---|---|---|
REQUIRED**(默认)** | 开启新的事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.REQUIRED) | 增删改方法 |
SUPPORTS | 不开启事务 | 融合到外部事务中 | @Transactional(propagation = Propagation.SUPPORTS) | 查询方法 |
REQUIRES_NEW | 开启新的事务 | 挂起外部事务,创建新的事务。 | @Transactional(propagation = Propagation.REQUIRES_NEW) | 日志记录方法中 |
NOT_SUPPORTED | 不开启事务 | 挂起外部事务 | @Transactional(propagation = Propagation.NOT_SUPPORTED) | 及其不常用 |
NEVER | 不开启事务 | 抛出异常 | @Transactional(propagation = Propagation.NEVER) | 及其不常用 |
MANDATORY | 抛出异常 | 融合到外部事务中 | @Transactional(propagation = Propagation.MANDATORY) | 及其不常用 |
11.4、只读属性
针对只进行查询操作的业务方法,可以加入只读属性(readlyonly = true
),提高运行的效率。他的默认值为false。
11.5、超时属性
当前的事务访问数据时,有可能访问的数据被别的事务进行加锁处理,那么此时本事务就需要进行等待。超时属性指定了事务等待的最长时间,这个时间是以秒为单位的。他的默认值是-1,他最终的时间由对应的数据库来决定。
应用示范:@Transaction(timeout=秒数)
;
11.6、异常属性
Spring事务处理过程中,默认对RuntimeException及其子类采用的是回滚策略,而对于Exception及其子类采用的是提交策略。
// 异常属性有两个值
// 1. rollbackFor();表示回滚的异常类型,里面写异常的类型的字节码对象,他里面的值的类型是数组类型,所以可以写多个异常类型的字节码对象,用逗号隔开
rollbackFor({java.lang.Exception.class});
// noRollbackFor();表示不会滚的异常类型的字节码对象
noRollbackFor({java.lang.RuntimeException.class})
11.7、事务属性及其常见配置总结
-
隔离属性:一般用默认值。
-
传播属性:一般增删改用Required(默认值),查询操作用Supports。
-
只读属性:一般增删改设置readOnly的值为false,查询设置readOnly的值为true。
-
超时属性:一般用默认值为-1。
-
异常属性:一般用默认值。
总结: -
增删改操作:
@Transactional
。 -
查询操作:
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
十二、Spring整合MVC
12.1、依赖准备
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
12.2、为什么要整合MVC框架
因为有很多的需求和功能是原生的Spring框架无法解决的,需要MVC框架来简化我们的开发。常用的MVC框架有:SpringMVC、Struts2、Struts1、jsf、webwork(除了第一个其他的都被淘汰了)。
- MVC框架提供了控制器(Controller),用来调用Service层。
- 请求响应的处理。
- 接受请求参数。
- 控制程序的运行流程。
- 视图解析(JSP、JSON、Freemarker、Thyemeleaf)
12.3、Spring整合MVC框架的核心思路
准备工厂
1. Web开发过程中如何创建工厂
ApplicationContext ctx = new WebXmlApplication("/applicationContext.xml");
2. 如何保证工厂被共用同时保证唯一性
被共用:在Web中有四个作用于:request、session、ServletContext(application),我们可以把工厂存储到ServletContext这个作用域中。
唯一性:我们可以利用ServletContextListener这个监听器,因为在ServletContext对象创建的同时,ServletContextListener会被调用且只被调用一次。所以我们可以把工厂创建的代码写在ServletContextListener中,这样就保证了唯一性。
3. 总结
我们首先在ServletContextListener这个监听器中书写创建工厂的代码,其次将这个工厂放在ServletContext对象中。
4. Spring的封装
既然这些代码如此繁琐,那么Spring肯定会帮我们封装好了。
5. ContextLoaderListener类
Spring封装了一个ContextLoaderListener类,他的作用是创建工厂和将工厂放进ServletContext对象中。
配置文件
既然Spring帮我们封装好了一个ContextLoaderListener类,那么我们直接按照监听器在web.xml中进行配置即可。
<!---我们只需要在web.xml中进行配置即可-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--告诉Spring我们的配置文件在哪里-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
代码整合
代码整合的核心是依赖注入,将控制器需要的Service对象注入到控制器对象中。