一、Spring 概述
1.1 简介
-
Spring:春天 --> 给软件行业带来了春天;
2002,首次推出了 Spring 框架的雏形
interface21
框架;Spring 框架,即以 interface21 框架为基础,经过重新设计,并不断丰富内涵,于 2004 年 3 月 24 日,发布了 1.0 正式版;
Rod Johnson:Spring Framework 创始人;
Spring 理念:使现有的技术更加容易使用,本身是一个大杂烩,整合了现有的技术框架。
SSH:Struct2 + Spring + Hibernate
SSM:SpringMVC + Spring + Mybatis
官网:链接
下载地址:链接
Github:链接
-
Maven 依赖:
- 导入
webmvc
会自动导入相关依赖; -
jdbc
用于和 Mybatis 整合;
- 导入
org.springframework
spring-webmvc
5.3.17
org.springframework
spring-jdbc
5.3.17
1.2 优点
- Spring 是一个开源的免费的框架(容器);
- Spring 是一个轻量级的、非入侵式的框架;
- 控制反转(IOC)、面向切面编程(AOP);
- 支持事务的处理,对框架整合的支持;
- ==总结一句话:Spring 就是,轻量级的控制反转(IOC)和面向切面(AOP)编程的框架==
1.3 组成
Spring 框架是一个分层架构,由 7 个定义良好的模块组成;
-
Spring 模块,构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式;
组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现;
模块简介:
- 核心容器:
- 提供 Spring 框架的基本功能;
- 主要组件是
BeanFactory
,它是工厂模式的实现,使用控制反转(IOC)模式,将应用程序的配置,和依赖性规范与实际的应用程序代码分开;
- Spring 上下文:
- 是一个配置文件,向 Spring 框架提供上下文信息,包括企业服务,例如:
JNDI
、EJB
、电子邮件、国际化、校验和调度功能;
- 是一个配置文件,向 Spring 框架提供上下文信息,包括企业服务,例如:
- Spring AOP:
- 面向切面的编程功能,可管理任何支持 AOP 的对象;
- 基于 Spring 的应用程序中的对象,提供了事务管理服务;
- 不用依赖组件,就可以将声明性事务管理,集成到应用程序中;
- Spring DAO:
-
JDBC DAO
抽象层,提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息; - 异常层次结构,简化了错误处理,降低了需要编写的异常代码数量(例如打开和关闭连接);
-
Spring DAO
的面向 JDBC 的异常,遵从通用的 DAO 异常层次结构;
-
- Spring ORM:
- Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括
JDO
、Hibernate
和iBatis SQL Map
; - 遵从 Spring 的通用事务和 DAO 异常层次结构;
- Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括
- Spring Web 模块:
- 建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文;
- Spring 框架支持与
Jakarta Struts
的集成; - 简化了处理多部分请求,以及将请求参数绑定到域对象的工作;
- Spring MVC 框架:
- 全功能的构建 Web 应用程序的 MVC 实现;
- 通过策略接口,MVC 框架变成为高度可配置的;
- MVC 容纳了大量视图技术,其中包括
JSP
、Velocity
、Tiles
、iText
和POI
;
1.4 拓展
-
Spring Boot:
- 快速开发的脚手架;
- 基于 Spring Boot,可以快速的开发单个微服务;
- 约定大于配置;
-
Spring Cloud:
- SpringCloud 是基于 SpringBoot 实现的;
学习 SpringBoot 的前提,需要完全掌握 Spring 以及SpringMVC,承上启下的作用;
弊端:发展了太久之后,违背了原来的理念,配置十分繁琐;
二、IOC 基础
- 新建空白的 maven 项目;
2.1 IOC 理论推导
以前代码的实现方式:
- 创建 UserDao 接口:
public interface UserDao {
public void getUser();
}
- 创建 UserDaoImpl 实现类 :
public class UserDaoImpl implements UserDao{
@Override
public void getUser() {
System.out.println("获取用户数据");
}
}
- 创建 UserService 业务接口:
public interface UserService {
public void getUser();
}
- 创建 UserServiceImpl 业务实现类:
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
@Override
public void getUser() {
userDao.getUser();
}
}
- 测试:
public class MyTest {
@Test
public void testUser() {
UserService service = new UserServiceImpl();
service.getUser();
}
}
以前增加需求的实现方式:
- 增加 Userdao 的实现类:
public class UserDaoOracleImpl implements UserDao{
@Override
public void getUser() {
System.out.println("Oracle数据");
}
}
- 在 UserServiceImpl 实现类里,修改对应的实现:
public class UserServiceImpl implements UserService {
// 修改UserDao的对应实现类
private UserDao userDao = new UserDaoOracleImpl();
@Override
public void getUser() {
userDao.getUser();
}
}
- 发现问题:用户的需求变化,会影响内部的实现代码,需要根据用户的需求去修改源代码;
解决方案:
- 使用 Set 接口,实现不同的需求:
public class UserServiceImpl implements UserService {
private UserDao userDao;
// 利用set进行动态实现值的注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void getUser() {
userDao.getUser();
}
}
- 测试:
public class MyTest {
@Test
public void testUser() {
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(new UserDaoImpl());
service.getUser();
// 用Oracle去实现
service.setUserDao(new UserDaoOracleImpl());
service.getUser();
}
}
小结:
-
实现方式对比:
之前,程序是主动创建对象,控制权在程序员;
使用了 set 注入后,程序不再具有主动性,而是变成了被动的接收对象;
这种思想,从本质上解决了问题,程序员不用再去管理对象的创建,只专注于业务的实现,使系统的耦合性大大降低,这就是 IOC 的原型;
2.2 IOC 本质
控制反转 IOC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现 IOC 的一种方法(也有人认为 DI 只是 IOC 的另一种说法);
没有 IOC 的程序中,使用面向对象编程,对象的创建与对象间的依赖关系,完全硬编码在程序中,对象的创建由程序自己控制,控制反转后,将对象的创建转移给第三方;
-
所谓控制反转,就是获得依赖对象的方式反转了;
-
IOC 是 Spring 框架的核心内容,实现方式:
- 使用 XML 配置;
- 使用注解;
- 新版本的 Spring 可以零配置实现 IOC;
-
Spring 容器在初始化时,先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时,再从 IOC 容器中取出需要的对象;
采用 XML 方式配置 Bean 时,Bean 的定义信息是和实现分离的,而采用注解的方式,可以把两者合为一体,Bean 的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的;
控制反转,是一种通过描述(XML 或注解)并通过第三方,去生产或获取特定对象的方式;
在 Spring 中,实现控制反转的是 IOC 容器,其实现方法是,依赖注入(Dependency Injection,DI);
三、Hello Spring
3.1 搭建环境
- 导入相关依赖,spring 需要导入commons-logging 进行日志记录,maven 会自动下载对应的依赖项;
org.springframework
spring-webmvc
5.3.17
3.2 编码代码
- 创建实体类:
Hello.java
public class Hello {
private String str;
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
public void show() {
System.out.println("Hello " + str);
}
}
- 创建 spring 配置文件,
beans.xml
(文件名可自定义):
- 测试:
public class MyTest {
@Test
public void helloTest() {
// 获取beans.xml:拿到Spring管理对象的容器
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// genBean:参数就是Spring配置文件中bean的id(对象名)
Hello hello = (Hello) context.getBean("hello");
hello.show();
}
}
3.3 思考
- Hello 对象是谁创建?
- 由 Spring 创建;
- Hello 对象的属性是怎么设置的?
- 由 Spring 容器设置的;
这个过程就叫做 控制反转:
-
控制:控制对象的创建;
- 传统应用程序,对象是由程序本身控制创建;
- 使用 Spring 后,对象是由 Spring 来创建;
反转:程序本身不创建对象,而变成被动的接收对象;
依赖注入:利用 set 方法来进行注入;
IOC 是一种编程思想,由主动的编程,变成被动的接收;
可以通过
new ClassPathXmlApplicationContext
查看底层源码;
3.4 修改之前代码
-
IDEA 快捷创建
beans.xml
文件,自动导入 spring 配置信息: -
配置上下文:按提示操作
bean 对象添加:
- 测试:
@Test
public void testSpring() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService service = (UserService) context.getBean("service");
service.getUser();
}
小结:
- 要实现不同的操作,不用在程序中去改动,只需要在 xml 配置文件中进行修改;
- 所谓的 IOC,就是对象由 Spring 来创建、管理、装配;
四、IOC 创建对象方式
4.1 通过无参构造(默认)
- 创建实体类:无参构造
public class User {
private String name;
public User() {
System.out.println("无参构造");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show() {
System.out.println("name=" + name);
}
}
- 创建配置文件:
beans.xml
- 测试:
@Test
public void testUser() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 在执行getBean的时候,user已经通过无参构造创建好了
User user = (User) context.getBean("user");
// 调用对象的方法
user.show();
}
-
运行结果:
在调用 show 方法之前,User 对象,已经通过无参构造初始化了;
4.2 通过有参构造
- 创建实体类:有参构造
public class User2 {
private String name;
public User2(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show() {
System.out.println("name=" + name);
}
}
有参构造
beans.xml
的三种创建方式
- 下标赋值:
- 类型赋值:不建议使用,重复类型难以分辨
- 参数名赋值:
- 测试:
@Test
public void testUser2() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User2 user2 = (User2) context.getBean("user2");
user2.show();
}
小结:
- 在配置文件加载时,容器中管理的对象,就已经初始化了;
五、Spring 配置
5.1 别名
- alias:设置别名;
5.2 Bean 的配置
- bean 就是 java 对象,由 Spring 创建和管理:
5.2 import
- 一般用于团队开发,可以将多个配置文件,导入合并为一个;
-
applicationContext.xml
(总配置文件):- 使用时,直接使用总的配置就可以;
六、依赖注入(DI)
6.1 构造器注入
- 查看上文;
6.2 set 注入(重点)
- 依赖注入(Dependency Injection,DI):set 方法注入
- 依赖:指 Bean 对象的创建,依赖于容器;
- 注入:Bean 对象中的所有属性,由容器来注入;
搭建环境
-
要求被注入的属性,必须有 set 方法:
- 方法名由 set + 属性首字母大写;
- 属性是 boolean 类型,没有set方法,是 is;
创建实体类:复杂类型(引用类)
public class Address {
private String address;
// get、set、toString
}
- 真实测试对象:
public class Student {
private String name;
private Address address;
private String[] books;
private List hobbys;
private Map card;
private Set games;
private String wife;
private Properties info;
// get、set、toString
}
- 配置文件:
applicationContext.xml
- 测试:
@Test
public void studentTest() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Student student = (Student) context.getBean("student");
System.out.println(student.getName());
}
- 完善注入信息:
西游记
红楼梦
水浒传
爬山
阅读
听歌
LOL
BOB
COC
com.mysql.cj.jdbc.Driver
jdbc:mysql://localhost:3306/数据库名?
root
123456
- 测试:
@Test
public void studentTest() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Student student = (Student) context.getBean("student");
System.out.println(student.toString());
/*
Student{
name='学生1',
address=Address{address='null'},
books=[西游记, 红楼梦, 水浒传],
hobbys=[爬山, 阅读, 听歌],
card={建行=217842215439, 工行=54358942439},
games=[LOL, BOB, COC],
wife='null',
info={
password=123456,
driver=com.mysql.cj.jdbc.Driver,
url=jdbc:mysql://localhost:3306/数据库名?,
username=root
}
}
*/
}
}
6.3 拓展方式注入
使用
c
和p
命名空间,进行注入;-
官方解释:
创建实例类:
public class User {
private String name;
private int age;
// // get、set、toString
}
- 创建配置文件:
beans.xml
- 测试:
@Test
public void testUser() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
User user2 = (User) context.getBean("user2");
System.out.println(user);
System.out.println(user2);
}
- 注意点:
- p 和 c 命名空间,不能直接使用,需要导入
xml
头文件约束; - c 命名空间,通过构造器注入,必须定义无参构造(否则报错);
- p 和 c 命名空间,不能直接使用,需要导入
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
6.4 Bean 的作用域
-
六种模式:
单例模式(Spring 默认机制):singleton
-
get 到的都是同一个对象:
配置:
- 测试:
@Test
public void testUser2() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
User user2 = (User) context.getBean("user");
System.out.println(user==user2); // true
}
原型模式:prototype
-
每次从容器中 get 时,都产生一个新的对象:
配置:
- 测试:
@Test
public void testUser2() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
User user2 = (User) context.getBean("user");
System.out.println(user==user2); // false
}
-
request
、session
、application
只能在 web 开发中使用;
七、Bean 的自动装配
- 自动装配是 Spring 满足 bean 依赖的一种方式;
- Spring 会在上下文中自动寻找,并自动给 bean 装配属性;
- Spring 中的三种装配方式:
- 在 xml 中显式配置;
- 在 Java 中显式配置;
- 隐式的自动装配 bean(重点);
7.1 搭建测试环境
创建项目:
一个人有两个宠物;
实体类:Cat
public class Cat {
public void shout() {
System.out.println("miao~");
}
}
- 实体类:Dog
public class Dog {
public void shout() {
System.out.println("wang~");
}
}
- 实体类:People
public class People {
private Cat cat;
private Dog dog;
private String name;
// get、set、toString
}
- 创建 Spring 配置文件:
- 测试:
@Test
public void myTest() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
People people = (People) context.getBean("people");
people.getCat().shout();
people.getDog().shout();
}
-
查看运行结果:
7.2 ByName 自动装配
- 修改 bean 配置,增加属性
autowire="byName"
:
- 当 bean 节点有
autowire="byName"
属性时:- 查找此类中,所有的 set 方法名(去掉 set 后,首字母小写 );
- 去 spring 容器中,寻找对应 set 方法名的 id 对象;
- 如果有,就取出注入,如果没有,就报空指针异常;
7.3 ByType 自动装配
- 修改 bean 配置,
autowire="byType"
:- 被引用的 bean 不再需要 id;
- 类型:必须保证全局唯一,否则报错;
小结:
- byName:
- 必须保证所有 bean 的 id 唯一;
- bean 需要和自动注入属性的 set 方法值一致;
- byType:
- 必须保证所有 bean 的 class 唯一;
- bean 需要和自动注入属性的类型一致;
7.4 使用注解实现自动装配
jdk1.5 支持注解,Spring2.5 开始支持注解;
-
使用注解实现自动装配,需重新配置 xml 头部信息:
- 导入 context 约束;
- 开启属性注解支持:
;
@Autowired
-
@Autowired
是按类型自动转配的,不支持 id 匹配; - 需要导入
spring-aop
的包,或依赖; - 使用方式:
- 直接在属性上使用即可;
- 也可以在 set 方式上使用;
- 注:使用 @Autowired 可以不用写 Set 方法,前提:
- 自动装配的属性在 IOC(Spring)容器中存在;
- 且符合名字 byName;
- 修改:在类中去掉 Set 方法,使用
@Autowired
注解;
public class People {
@Autowired
private Cat cat;
@Autowired
private Dog dog;
private String name;
// get、toString
}
- 修改配置文件:
拓展
-
@Nullable
:字段可以为 nullpublic People(@Nullable String name) { this.name = name; }
-
@Autowired(required=false)
:- 默认:true(必须存在对象,不能为 null)
// Autowired 源码 public @interface Autowired { boolean required() default true; }
-
false
:对象可以为 null;
// 如果允许对象为 null,设置required = false,默认为true @Autowired(required = false) private Cat cat;
@Qualifier
@Autowired
是根据类型自动装配,加上@Qualifier
就可以根据 byName 的方式自动装配;@Qualifier 不能单独使用;
-
测试:
- 修改配置文件内容,类型不变,名字不为类的默认名字:
- 在属性上添加 @Qualifier 注解:
@Autowired
@Qualifier(value = "cat1")
private Cat cat;
@Autowired
@Qualifier(value = "dog1")
private Dog dog;
小结:
- 没有加
@Qualifier
测试,会直接报错; - 如果
@Autowired
自动装配的环境比较复杂,自动装配无法通过@Autowired
一个注解完成时,可以使用@Qualifier(value=“xxx”)
去配置@Autowired
的使用,指定一个唯一的 bean 对象注入;
@Resource:Java 注解
-
JDK11 以上,包被移除了,需要导入包或依赖:
javax.annotation javax.annotation-api 1.3.2 -
修改实体类注解:
@Resource(name = "cat11") private Cat cat; @Resource private Dog dog; private String name;
-
修改配置文件:
小结:
-
@Resource 和 @Autowired 的区别:
都是用来 自动装配 的,都可以放在属性字段上;
@Autowired
通过 byType 的方式实现,而且必须要求这个对象存在;【常用】@Resource
默认通过 byName 的方式实现,如果找不到名字,则通过 byType 实现,如果两个都找不到的情况下,就报错;【常用】执行顺序不同:
@Autowired
通过 byType 的方式实现,@Resource
默认通过 byName 的方式实现;
注意:byType 的类型,必须全局唯一;
八、使用注解开发
环境配置:
-
spring 4 之后,想要使用注解,必须要引入 aop 的包;
配置文件中,要引入 context 约束,增加注解的支持:
8.1 Bean 的实现
- 之前是使用 bean 的标签,进行 bean 注入,但实际开发中,一般会使用注解;
- 配置扫描哪些包下的注解:
- 在 dao 包下创建类,增加注解:
/*
@Component:组件
相当于配置文件中
可以指定对象名:@Component("对象名"),默认:类名首字母小写
*/
@Component
public class User {
public String name;
}
8.2 属性注入
- 不需要 set 方法,属性名上直接添加:
@value("值")
:
@Component
public class User {
// 相当于配置文件中
@Value("测试")
public String name;
}
- 如果提供了 set 方法,在 set 方法上添加
@value("值")
:
@Value("测试")
public void setName(String name) {
this.name = name;
}
- 测试:
@Test
public void testUser() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
System.out.println(user.name);
}
8.3 衍生注解
-
@Component
的衍生注解:web 开发中,按 mvc 三层架构分层;-
@Repository
:dao 层; -
@Service
:service 层; -
@Controller
:web 层;
-
四个注解功一样:将类注册到 Spring 中,装配 Bean;
8.4 自动装配注解
-
@Autowired
:- 自动装配,通过类型,名字;
- 如果
@Autowired
不能唯一自动装配上属性,则需要通过@Qualifier(value="xxx")
;
-
@Nullable
:字段标记了这个注解,说明这个字段可以为 null; -
@Resource
(Java 注解):自动装配,通过名字,类型;
8.5 作用域
- @Scope:
- singleton(默认):单例模式,创建这个对象,关闭工厂,所有的对象都会销毁;
- prototype:原型模式,关闭工厂,所有的对象不会销毁,内部的垃圾回收机制会回收;
@Component
@Scope("prototype")
public class User {
public String name;
}
小结:
- xml 与注解:
- xml 更加万能,适用于任何场合,维护简单方便;
- 注解,不是自己的类无法使用,维护相对复杂;
- xml 与注解整合开发,推荐最佳实践方式:
- xml 用来管理 bean;
- 注解,只负责完成属性的注入;
- 在使用的过程中,需要注意:
- 让注解生效,必须开启注解的支持;
九、使用 Java 的方式配置 Spring
- JavaConfig 原来是 Spring 的一个子项目;
- 它通过 Java 类的方式,提供 Bean 的定义信息;
- 在 Spring4 的版本,JavaConfig 已正式成为 Spring4 的核心功能;
搭建环境
- 创建项目;
- 创建实体类:
// @Component:将这个类,标注为Spring的一个组件,放到容器中
@Component
public class User {
private String name;
public String getName() {
return name;
}
@Value("测试")
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
- 新建 config 配置包,创建 MyConfig 配置类:
/*
@Configuration:
代表这是一个配置类,等同于beans.xml
本身就是一个组件(@Component),
也会被Spring容器托管,注册到容器中
*/
@Configuration
// 可以设置扫描包
@ComponentScan("com.study.spring.config")
// 可以引入其它配置类
@Import(MyConfig2.class)
public class MyConfig {
/*
注册一个bean,相当于bean标签
方法名:相当于bean标签中的id属性
方法的返回值:相当于bean标签中的class属性
*/
@Bean
public User getUser() {
// 返回要注入到bean的对象
return new User();
}
}
- 测试:
@Test
public void testUser() {
// 使用了配置类方式,只能通过AnnotationConfig上下文来获取容器,通过配置类的class对象加载
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
// getBean的参数为,@Bean对应的方法名,非类名首字母小写!!
User user = (User) context.getBean("getUser");
System.out.println(user.getName());
}
- 纯 Java 的配置方式,SpringBoot 中比较常见;
十、代理模式
-
为什么要学习代理模式?
- Spring AOP 的底层就是代理模式;
-
代理模式的分类:
- 静态代理;
- 动态代理;
10.1 静态代理
- 静态代理角色分析:
- 抽象角色:一般使用接口或者抽象类来实现;
- 真实角色:被代理的角色;
- 代理角色:代理真实角色,代理后 , 会增加附属操作;
- 客户:访问代理对象的人;
代码实现
- 抽象角色:租房
// 租房
public interface Rent {
public void rent();
}
- 真实角色:房东
// 房东:实现租房接口
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东出租房屋...");
}
}
- 代理角色:代理人
// 代理:代理房东、实现租房接口、并增加附属操作
public class Proxy implements Rent {
private Host host;
public Proxy() {
}
// 有参构造方式,注入真实对象
public Proxy(Host host) {
this.host = host;
}
@Override
public void rent() {
seeHouse();
// 房东租房
host.rent();
hetong();
fare();
}
// 看房
public void seeHouse() {
System.out.println("中介带看房");
}
// 签合同
public void hetong() {
System.out.println("签订合同");
}
// 收中介费
public void fare() {
System.out.println("收中介费");
}
}
- 客户:客户端访问代理角色
// 客户
public class Client {
public static void main(String[] args) {
// 房东要租房子
Host host = new Host();
// 代理:中介代理房东出租房子,但是,代理会增加附属操作
Proxy proxy = new Proxy(host);
// 客户不用面对房东,直接找中介即可
proxy.rent();
}
}
- 代理模式的好处:
- 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务;
- 公共业务交给代理角色,实现了业务的分工;
- 公共业务发生扩展时,方便集中管理;
- 缺点:
- 一个真实角色,就会产生一个代理角色,代码量会翻倍,开发效率会变低;
10.2 加深理解
静态代理实现 CRUD
- 抽象角色:用户业务(增、删、改、查)
// 抽象角色:增删改查业务
public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}
- 真实对象:完成增、删、改、查操作
// 真实对象:完成增删改查操作
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("增加一个用户");
}
@Override
public void delete() {
System.out.println("删除一个用户");
}
@Override
public void update() {
System.out.println("修改一个用户");
}
@Override
public void query() {
System.out.println("查询用户");
}
}
- 代理角色:通过代理类,增加日志功能
// 代理角色:增加日志的实现
public class UserServiceProxy implements UserService {
public UserServiceImpl userService;
// set方法,注入真实对象(注入方式:有参构造、set方法)
public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}
@Override
public void add() {
log("add");
userService.add();
}
@Override
public void delete() {
log("delete");
userService.delete();
}
@Override
public void update() {
log("update");
userService.update();
}
@Override
public void query() {
log("query");
userService.query();
}
// 日志方法
public void log(String msg) {
System.out.println("[Debug] 使用了" + msg + " 方法");
}
}
- 测试:
@Test
public void testUser() {
// 真实业务
UserServiceImpl userService = new UserServiceImpl();
// 代理类
UserServiceProxy proxy = new UserServiceProxy();
// 使用代理类,增加了日志功能的实现
proxy.setUserService(userService);
proxy.add();
proxy.delete();
proxy.update();
proxy.query();
}
-
运行结果:
-
AOP 核心思想:
- 不改变原有代码,通过代理,实现对原有功能的增强;
10.3 动态代理
动态代理和静态代理角色一样;
动态代理的代理类,是动态生成的,不是直接写好的;
-
动态代理分为两大类:
- 基于接口的动态代理:JDK 动态代理;
- 基于类的动态代理:
cglib
; - Java 字节码实现:
javasisit
;
-
JDK 动态代理,需要了解两个类:
Proxy:代理;
InvocationHandler:调用处理程序;
InvocationHandler
- 用来生成代理的类,不需要去为每个代理,都单独生成一个类;
/*
处理代理实例,并返回结果
参数:
proxy:代理类,代理的真实代理对象;
method:要调用某个对象真实的方法的Method对象;
args:代理对象方法传递的参数;
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(target, args);
return result;
}
Proxy
- Proxy 类:用来创建一个代理对象的类;
- 常用方法:
newProxyInstance()
;
代码实现
抽象角色、真实角色,参照 10.2
创建自动生成代理的类:
// 用这个类,自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {
// 被代理的接口
private Object target;
public void setTarget(Object target) {
this.target = target;
}
// 生成得到代理类(固定代码)
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
@Override
// 处理代理实例,并返回结果
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 动态代理的本质,就是使用反射机制实现
log(method.getName());
Object result = method.invoke(target, args);
return result;
}
// 日志方法
public void log(String msg) {
System.out.println("[Debug] 使用了" + msg + " 方法");
}
}
- 测试:
- 创建真实角色;
- 创建动态代理创建的实体;
- 设置;
- 使用;
public class Client {
public static void main(String[] args) {
// 真实角色
UserServiceImpl userService = new UserServiceImpl();
// 代理角色,不存在,需要动态创建
ProxyInvocationHandler pih = new ProxyInvocationHandler();
// 设置要代理的对象
pih.setTarget(userService);
// 动态生成代理类
UserService proxy = (UserService) pih.getProxy();
proxy.add();
proxy.delete();
proxy.update();
proxy.query();
}
}
- 如果需要生成多个代理对象,只需要去创建不同的 userService 和其方法,无需再对每个代理进行设置;
动态代理的好处
- 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务;
- 公共业务交给代理角色,实现了业务的分工;
- 公共业务发生扩展时,方便集中管理;
- 一个动态代理类,代理的是一个接口,一般就是对应的一类业务;
- 一个动态代理类,可以代理多个类,需要实现同一个接口;
十一、AOP
11.1 什么是 AOP
-
AOP(Aspect Oriented Programming):面向切面编程
- 通过预编译方式,和运行期动态代理,实现程序功能统一维护的一种技术;
- AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring 框架中的一个重要内容,是函数式编程的一种衍生泛型;
- 利用 AOP,可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑,各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率;
11.2 Aop 在 Spring 中的作用
提供声明式事务,允许用户自定义切面;
-
横切关注点:
- 跨越应用程序多个模块的方法或功能;
- 与业务逻辑无关,但是需要关注的部分,就是横切关注点,如日志、安全、缓存、事务,等等;
切面(ASPECT):横切关注点,被模块化的特殊对象,是一个类;
通知(Advice):切面必须要完成的工作,是类中的一个方法;
目标(Target):被通知对象;
代理(Proxy):向目标对象应用通知之后,创建的对象;
切入点(PointCut):切面通知,执行的 地点 的定义;
-
连接点(JointPoint):与切入点匹配的执行点;
-
Spring AOP 中,通过 Advice 定义横切逻辑,Spring 中支持 5 种类型的 Advice:
即 Aop 在不改变原有代码的情况下,去增加新的功能;
11.3 使用 Spring 实现 Aop
- 使用 AOP 织入,需要导入
aspectjweaver
依赖:
org.aspectj
aspectjweaver
1.9.8
第一种方式:通过 Spring API 实现
- 创建接口:
public interface UserService {
public void add();
public void delete();
public void update();
public void select();
}
- 创建实现类:
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加一个用户");
}
@Override
public void delete() {
System.out.println("删除一个用户");
}
@Override
public void update() {
System.out.println("修改一个用户");
}
@Override
public void select() {
System.out.println("查询用户");
}
}
- 前置增强
MethodBeforeAdvice
:AOP 增加的业务(前置日志)
public class Log implements MethodBeforeAdvice {
/*
method:要执行的目标对象的方法
args:参数
target:目标对象
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了");
}
}
- 后置增强
AfterReturningAdvice
:AOP 增加的业务(后置日志)
public class AfterLog implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
// returnValue:返回值
System.out.println("执行了" + method.getName() + "方法,返回值:" + returnValue);
}
}
- 在 spring 的配置文件中,注册 bean,并实现 aop 切入:
- 头文件,需导入 Aop 约束;
- 测试:
@Test
public void testUser() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 增加 UserService.class 后,不再需要强制转换
// 注意点:动态代理,代理的是接口,不是实现类
UserService userService = context.getBean("userService", UserService.class);
userService.add();
userService.delete();
userService.update();
userService.select();
}
- 注意点:动态代理,代理的是接口,不是实现类;
第二种方式:自定义类(切面),实现 Aop
切面定义:自定义一个需要被插入的类;
自定义类(切面):
// 自定义切面(需要插入的类)
public class DiyPointCut {
public void before() {
System.out.println("=========方法执行前=========");
}
public void after() {
System.out.println("=========方法执行后=========");
}
}
- 配置 xml 文件:
- 测试:其它代码不变
切入点位置表达式
- 格式:
execution(* com.service ..*.*(..))
- 解释:
符号 | 含义 |
---|---|
execution() | 执行,表达式的主体 |
第一个 * | 表示返回值的类型任意 |
com.service | AOP 所切入的服务的包名,业务部分 |
包名后面的 .. | 表示当前包及子包 |
第二个 * | 表示类名,即所有类 |
.*(..) | 表示任何方法名,括号表示参数,两个点表示任何参数类型 |
- 第二种方式,实现更加简单,但是实现的功能少了,比如:不能使用反射,获取执行方法的名称;
第三种方式:使用注解实现
- 创建用注解实现的增强类:
// @Aspect:标注这个类是一个切面
@Aspect
public class AnnotationPointCut {
@Before("execution(* com.study.spring.service.UserServiceImpl.*(..))")
public void before() {
System.out.println("=========方法执行前=========");
}
@After("execution(* com.study.spring.service.UserServiceImpl.*(..))")
public void after() {
System.out.println("=========方法执行后=========");
}
// 环绕增强:可以给定义一个参数,代表要获取处理切入的点
@Around("execution(* com.study.spring.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕前");
// getSignature():方法签名,查看目标方法名
System.out.println(jp.getSignature());
// 执行目标方法
Object proceed = jp.proceed();
System.out.println(proceed);
System.out.println("环绕后");
}
}
- 配置 xml 文件,注册 bean,并增加支持注解的配置:
十二、整合 Mybatis
12.1 搭建环境
导入相关依赖
- junit:
junit
junit
4.13.2
- spring 相关:
org.springframework
spring-webmvc
5.3.17
- Java 注解:(JDK11 以上需要)
javax.annotation
javax.annotation-api
1.3.2
- mybatis:
org.mybatis
mybatis
3.5.9
- mybatis-spring 整合包 (重点):
org.mybatis
mybatis-spring
2.0.7
- mysql:
mysql
mysql-connector-java
8.0.28
- spring-jdbc:
org.springframework
spring-jdbc
5.3.17
- AOP 织入:
org.aspectj
aspectjweaver
1.9.8
- Maven 静态资源过滤:
src/main/java
**/*.properties
**/*.xml
true
12.2 MyBatis 项目回顾
- 创建实体类:
public class User {
private int id;
private String name;
private String pwd;
// get、set、toString
}
- 编写核心配置文件:
- 创建接口类:
public interface UserMapper {
public List selectUser();
}
- 创建
Mapper.xml
:
- 测试:
@Test
public void testUser() throws IOException {
String resource = "mybatis-config.xml";
InputStream in = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List userList = mapper.selectUser();
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
-
运行结果:
12.3 MyBatis-Spring
官方文档
MyBatis-Spring:将 MyBatis 代码,无缝整合到 Spring 中;
-
MyBatis-Spring 对应版本:
MyBatis-Spring 相关依赖:
org.mybatis
mybatis-spring
2.0.7
- 要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义:
- 一个
SqlSessionFactory
;-
SqlSessionFactory
需要一个DataSource
(数据源);
-
- 至少一个数据映射器类;
- 一个
整合实现方式一:SqlSessionTemplate
- 创建 Mybatis 配置文件:
mybatis-config.xml
- 创建配置文件:
spring-dao.xml
- 替换 mybaits 的数据源;
- 配置 SqlSessionFactory,关联 MyBatis;
- 注册 sqlSessionTemplate,关联 sqlSessionFactory;
- 增加 UserMapper 接口的实现类,私有化 sqlSessionTemplate:
public class UserMapperImpl implements UserMapper {
// 以前使用SqlSession操作,现在都使用SqlSessionTemplate
private SqlSessionTemplate sqlSession;
public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}
// 在接口实现类中,实现以前测试类的方法,并将方法值返回(使用时在spring中直接调用方法即可)
@Override
public List selectUser() {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}
}
- 创建总配置文件:
applicationContext.xml
- 引入
spring-dao.xml
; - 注册实现类:
UserMapperImpl
;
- 引入
- 测试:
@Test
public void testSpringMybatis() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
for (User user : userMapper.selectUser()) {
System.out.println(user);
}
}
整合实现方式二:继承 SqlSessionDaoSupport
- mybatis-spring1.2.3 版以上的才有;
- dao 层通过继承 Support 类,直接利用 getSqlSession() 获得,然后直接注入 SqlSessionFactory;
- 与方式一相比,不需要管理 SqlSessionTemplate,而且对事务的支持更加友好,可跟踪源码查看;
代码实现:
- 创建 UserMapper 接口实现类:
public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
@Override
public List selectUser() {
// getSqlSession()直接获取,不需要创建
return getSqlSession().getMapper(UserMapper.class).selectUser();
}
}
- 注册 bean:
- 测试:
@Test
public void testSpringMybatis2() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper = context.getBean("userMapper2", UserMapper.class);
for (User user : userMapper.selectUser()) {
System.out.println(user);
}
}
十三、声明式事务
13.1 回顾事务
事务:把一系列的动作,当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用;
-
事务四个属性:ACID
- 原子性(atomicity):事务是原子性操作,由一系列动作组成,事务的原子性,确保动作要么全部完成,要么完全不起作用;
- 一致性(consistency):一旦所有事务动作完成,事务就要被提交,数据和资源处于一种,满足业务规则的一致性状态中;
- 隔离性(isolation):可能多个事务,会同时处理相同的数据,因此每个事务,都应该与其他事务隔离开来,防止数据损坏;
- 持久性(durability):事务一旦完成,无论系统发生什么错误,结果都不会受到影响,通常情况下,事务的结果,被写到持久化存储器中;
未开启事务测试:
- 复制上例中的代码到新项目中;
- 在接口 UserMapper 中新增两个方法,增加和删除用户:
// 增加用户
public int addUser(User user);
// 删除用户
public int deleteUser(int id);
- 修改
Mapper.xml
文件,把 deletes 写错(模拟程序出错)
insert into user (id, name, pwd)
values (#{id}, #{name}, #{pwd});
deletes from user where id = #{id};
- 在接口实现类,添加增加、删除对应的方法:
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
@Override
public List selectUser() {
User user = new User(4, "小王", "123");
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
mapper.addUser(user);
mapper.deleteUser(1);
return mapper.selectUser();
}
@Override
public int addUser(User user) {
return getSqlSession().getMapper(UserMapper.class).addUser(user);
}
@Override
public int deleteUser(int id) {
return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
}
}
- 测试:
@Test
public void testSpringMybatis() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
for (User user : userMapper.selectUser()) {
System.out.println(user);
}
}
-
运行结果:
-
报错:sql 异常,delete 写错了;
-
结果:插入成功;
-
小结:
- 没有进行事务的管理,程序部分出现错误,依旧可以提交;
- 如果想让一组程序,都成功时才成功,有一个失败,就都失败,就需要事务;
13.2 Spring 中的事务管理
-
需要用到的依赖:
-
aspectjweaver
:org.aspectj aspectjweaver 1.9.8 -
spring-jdbc
:org.springframework spring-jdbc 5.3.17
-
Spring 在不同的事务管理 API 之上定义了一个抽象层,使得开发人员,不必了解底层的事务管理 API,就可以使用 Spring 的事务管理机制;
-
Spring 支持:
- 编程式事务管理:
- 代码嵌到业务方法中,来控制事务的提交和回滚;
- 缺点:必须在每个事务操作业务逻辑中,包含额外的事务管理代码;
- 声明式事务管理:AOP
- 代码与业务方法分离,以声明的方式,来实现;
- 将事务管理,作为横切关注点,通过 AOP 方法模块化;
- Spring 中通过 Spring AOP 框架,支持声明式事务 管理;
- 编程式事务管理:
spring 七种事务类型
事务类型 | 说明 |
---|---|
REQUIRED | (默认)支持当前事务,无事务,另起新事物 |
SUPPORTS | 支持当前事务,无事务,以非事务执行 |
MANDATORY | 以事务方式执行,无事务,抛异常 |
REQUIRES_NEW | 新建事务,若有旧事务,挂起 |
NOT_SUPPORTED | 不支持事务,如有事务,挂起 |
NEVER | 以非事务执行,有事务,抛异常 |
NESTED | 内切事务 |
配置 spring-dao.xml 文件
- 使用 Spring 管理事务,注意头文件的约束导入:tx
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
- 配置声明式事务:JDBC 事务
- 配置事务的通知:
- 配置事务切入:AOP(注意导入头文件)
-
测试:
运行出现异常(deletes 写错);
查看数据库,未插入成功;
-
更改删除语句后,运行正常;
小结:
- 为什么需要配置事务:
- 如果不配置事务,可能存在数据提交不一致的情况;
- 如果不在 Spring 中去配置声明式事务,就需要在代码中,手动配置事务;
- 事务在项目的开发中十分重要,涉及到数据的一致性,和完整性问题,不容马虎;