Spring通关笔记:从“Hello Bean”到循环依赖的奇幻漂流
在学习之前,容我先讲两句,本博客所有内容全部由本人自主完成。我也只是一个学习编程之路上(某站狂神),喜欢把所学的做成笔记或者博客,可以一起相互讨论,若有错误请在评论区指导一下
此外,你可能还需要这些文件
本文所有代码整合:点击跳转下载
本人个人博客网站:了解笨猫
一、 Spring介绍
Spring是一个开源的免费的框架
Spring是一个轻量级的、非入侵式的框架
控制反转IOC、面向切面AOP
支持事务的处理,对框架整合的支持
总结一句话:Spring就是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架
一般需要导入的spring依赖
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.2.0.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.0.RELEASE</version></dependency></dependencies>
二、Spring的组成及扩展
扩展
弊端:人称“配置地狱”,配置十分繁琐,所以后面需要学SpringBoot快速开发框架才解放
学习SpringBoot的前提,需要完全掌握Spring和SpringMVC
三、IOC理论推导
以前写代码就是 UserDao接口、UserDaoImpl实现类、UserService业务接口、UserServiceImpl业务实现类,dao里面有三个实现类,而service只有一个业务类,需要测试哪个实现类都需要去service里更改源代码显得特别繁琐,用户的需求可能会影响我们原来的代码,我们需要根据用户的需求修改代码。通过一个set接口实现注入,这就是控制反转的底层思想,程序员不用再管理对象的创建,系统的耦合性大大降低,可以更在专注业务的实现上——不碰代码,动态调用。
IOC的原型——可以在service业务实现类不直接写死了,留个心眼。
//写死-只调用一个dao实现类,调用其他的就需要改代码
public class UserServiceImpl implements UserService {private UserDao userDao = new UserDaoOracleImpl();@Overridepublic void getUser() {userDao.getUser();}
}//留心眼-通过set方法动态调用
public class UserServiceImpl implements UserService {private UserDao userDao;@Overridepublic void setUserDao(UserDao userDao) {this.userDao = userDao;}@Overridepublic void getUser() {userDao.getUser();}
}
四、IOC本质
控制反转IOC(lnversion of control),是一种设计思想,DI(依赖注入)是实现IOC的一种方法,也有人认为D!只是IOC的另一种说法。没有IOC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。
采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。
控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IOC容器,其实现方法是依赖注入(DependencyInjection,Dl)。
五、第一个Spring程序-HelloSpring
xml文件配置-来源于Spring官网
容器实例化-来源于Spring官网
以下是个小实例 ,先写一个类,通过xml文件配置,最后实际操作如何调用
package com.benmao.pojo;public class HelloSpring {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}public void show(){System.out.println("Hello"+name);}
}
必须得有set方法,核心就是依赖注入
<?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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><!--使用Spring来创建对象,在Spring都称为Bean--><bean id="hello" class="com.benmao.pojo.HelloSpring"><property name="name" value="笨猫"/></bean></beans>
以前的写法就是:类 变量名 = new 类(); 对Spring来说: id代表变量名,class代表new的对象,property相当于给对象的属性设置一个值
public class MyTest {public static void main(String[] args) {//获取Spring的上下午对象ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");//现在对象都给Spring管理了,要使用谁就把谁调出来HelloSpring helloSpring = (HelloSpring) context.getBean("hello");helloSpring.show();}
}
对象是由谁创建的?对象是由spring创建的。对象的属性是这么设置的?对象的属性是由spring容器设置的。这个过程就叫控制反转
控制:传统应用程序的对象是由程序本身控制创建的,使用Spring的对象是由Spring来创建的
反转:程序本身不创建对象,而变成被动的接收对象
IOC就是一种编程思想,由主动的编程变成被动的接收。所谓IOC,一句话搞定:对象由Spring来创建、管理、装配。
六、IOC创建对象的方式
1. 默认使用无参构造对象
2. 如果需要使用有参构造对象,一般有以下三种方法
<!-- 第一种:下标赋值--><bean id="user" class="com.benmao.pojo.User"><constructor-arg index="0" value="笨猫"/></bean>
<!-- 第二种:类型赋值--><bean id="user" class="com.benmao.pojo.User"><constructor-arg type="java.lang.String" value="笨猫"/></bean>
<!-- 第三种:直接通过参数名--><bean id="user" class="com.benmao.pojo.User"><constructor-arg name="name" value="笨猫"/></bean>
注意:在beans.xml配置文件加载的时候,容器中管理的对象就已经初始化好了(所有的bean所对应类的无参构造方法都会被调用),你想要哪个对象就去getBean
七、Spring配置说明
alias
这个标签可以给bean取个别名,比如有个bean的id为user
<alias name="user" alias="qojjdqodoqjqdjjqodj"/>
import
这个import,一般用于团队开发使用,他可以将多个配置文件,导入合并到一个ApplicationContext.xml去,可以在ApplicationContext.xml写下。
<import resource="beans.xml1"/>小王负责
<import resource="beans.xml2"/>小吴负责
<import resource="beans.xml"3/>小朱负责
Bean的配置
id: bean 的唯一标识符,也就是相当于我们学的对象名
class: bean对象所对应的全限定名——包名+类名
name: 也是别名,但比alias更好用,可以同时取多个别名,能够有随意的分隔符(逗号,空格,分号)
<bean id="userT" class="com.benmao.pojo.UserT" name="userT1, userT2; userT3 userT4"> </bean>
八、DI依赖注入环境
一共有三种依赖注入方式,第一种就是构造器注入,前文已讲,第二种注入方式——Set方式注入
依赖注入:依赖——bean对象的创建依赖于Spring容器;注入:bean对象中所有的属性,由容器来注入
Set方式注入
以下是一个例子,写了字符串、数组、各类集合以及空值的注入方式。准备文件Address.java、Student.java、beans.xml、测试类。
package com.benmao.pojo;public class Address {private String address;public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}@Overridepublic String toString() {return "Address{" +"address='" + address + '\'' +'}';}
}
package com.benmao.pojo;import java.awt.print.Book;
import java.util.*;public class Student {private String name;private Address address;private String[] books;private List<String> hobbies;private Map<String, String> card;private Set<String> games;private String kong;private Properties info;...get、set和toString方法...
}
<?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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="address" class="com.benmao.pojo.Address"/><bean id="student" class="com.benmao.pojo.Student"><!--普通值注入——value--><property name="name" value="笨猫"/><!--引用注入——bean注入,ref--><property name="address" ref="address"/><!--数组注入——array--><property name="books"><array><value>红楼梦</value><value>西游记</value><value>水浒传</value><value>三国演义</value></array></property><!--list集合注入——list--><property name="hobbies"><list><value>敲代码</value><value>旅游</value><value>睡觉</value></list></property><!--map集合注入——map--><property name="card"><map><entry key="身份证" value="360124"/><entry key="银行卡" value="111111"/></map></property><!--Set集合注入注入——set--><property name="games"><set><value>王者</value><value>金铲铲</value><value>部落</value></set></property><!--空值注入--><property name="kong"><null/></property><!--Properties注入--><property name="info"><props><prop key="学号">20220819</prop><prop key="性别">男</prop><prop key="username">Benmao</prop><prop key="password">123456</prop></props></property></bean></beans>
public class MyTest {public static void main(String[] args) {ApplicationContext contest = new ClassPathXmlApplicationContext("beans.xml");Student student = (Student) contest.getBean("student");System.out.println(student);}
}
CP——c命名和p命名注入
要使用这两个注入方式需要在配置文件写入对应标签的代码
p标签代码:xmlns:p="http://www.springframework.org/schema/p"
看以下代码,可以发现p注入方式<property name=""></property>标签的简化,可以直接注入属性
<bean id="user" class="com.benmao.pojo.User" p:name="笨猫" p:age="18"/>
//测试一下@Testpublic void test2(){ApplicationContext contest = new ClassPathXmlApplicationContext("userbeans.xml");User user = contest.getBean("user", User.class);System.out.println(user);}
c标签代码:xmlns:c="http://www.springframework.org/schema/c"
使用c注入方式必须得有有参构造器,在User类里需要补一个有参构造器
public User(String name, int age) {this.name = name;this.age = age;}
看以下代码,可以发现c注入方式<constructor-arg name="" value=""/>标签的简化,通过有参构造注入属性
<bean id="user2" class="com.benmao.pojo.User" c:name="笨猫1" c:age="19" />
九、Bean的作用域
Spring官网给出了好几个作用域
singleton
单例模式,Spring默认机制,全局只共享一个对象
//输出的结果为true@Testpublic void test2(){ApplicationContext contest = new ClassPathXmlApplicationContext("userbeans.xml");User user = contest.getBean("user", User.class);User user2 = contest.getBean("user", User.class);System.out.println(user==user2);}
想要把bean变为单例模式,也可以在bean标签显式的添加singleton
<bean id="user3" class="com.benmao.pojo.User" scope="singleton"/>
prototype
原型模式,每一次从容器中getBean都会产生一个新对象
<bean id="user" class="com.benmao.pojo.User" scope="prototype"/>
//输出的结果为false@Testpublic void test2(){ApplicationContext contest = new ClassPathXmlApplicationContext("userbeans.xml");User user = contest.getBean("user", User.class);User user2 = contest.getBean("user", User.class);System.out.println(user==user2);}
扩展: request、session、application、websocket, 这些只能在web开发中使用到
十、Bean的自动装配
自动装配是Spring满足bean依赖的一种方式,Spring会在上下文中自动寻找,并自动给bean装配属性(前文都是需要自己手动装配)。在Spring中,有三种装配的方式
1. 在xml中显示的配置(前文就是xml手动配置)
2. 在Java中显示配置
3. 隐式的自动装配【重要】
测试:一个人有两只宠物
//猫
package com.benmao.pojo;public class Cat {public void shout(){System.out.println("cat shout");}
}//狗
package com.benmao.pojo;public class Dog {public void shout(){System.out.println("dog shout");}
}//人
package com.benmao.pojo;public class Person {private Cat cat;private Dog dog;private String name;...get、set、toString..
}
<bean id="cat" class="com.benmao.pojo.Cat"/><bean id="dog" class="com.benmao.pojo.Dog"/><bean id="person" class="com.benmao.pojo.Person"><property name="name" value="笨猫"/><property name="cat" ref="cat"/><property name="dog" ref="dog"/></bean>
@Testpublic void test1() {ApplicationContext contest = new ClassPathXmlApplicationContext("beans.xml");Person person = contest.getBean("person", Person.class);person.getCat().shout();person.getDog().shout();}
byName自动装配
会自动在容器中上下文中查找,和自己对象set方法后面对应的值对应的 bean的 id。也是就是说person中set有对应的cat和dog,beans中也有bean cat和dog,就会自动装配到person里
也就是说,比如dog不能是dog1111,否则报错
<bean id="person" class="com.benmao.pojo.Person" autowire="byName"><property name="name" value="笨猫"/></bean>
byType自动装配
会自动在容器中上下文中查找,和自己对象属性相同的 bean,所以id都可以不用写。也是就是说person中set有对应的cat和dog,beans中也有bean cat和dog,就会自动装配到person里
<bean id="cat" class="com.benmao.pojo.Cat"/><bean id="dog111" class="com.benmao.pojo.Dog"/>只能有一个对应类型的bean
<!-- <bean id="dog222" class="com.benmao.pojo.Dog"/> --><bean id="person" class="com.benmao.pojo.Person" autowire="byType"><property name="name" value="笨猫"/></bean>
但是这种装配方式必须报错只有一个唯一类型的bean
注解实现自动装配
使用注解版本:jdk1.5以上、Spring2.5以上
要使用注解,就需要context导入约束和配置注解的支持<context:annotation-config/>
配置约束
<?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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://www.springframework.org/schema/context/spring-context.xsd"><context:annotation-config/></beans>
@Autowired
直接在属性上使用即可,也可以在set方法上使用。其实可以都不需要写person里的set方法,因为它是通过反射来注入的
@AutorwiAured优先按类型查找,找不到报错,如果找到多个就按名字
在Person类中,对应属性上写上注解@Autowiredprivate Cat cat;@Autowiredprivate Dog dog;private String name;
写上这一行,开启注解的支持<context:annotation-config/><bean id="cat" class="com.benmao.pojo.Cat"/><bean id="dog" class="com.benmao.pojo.Dog"/><bean id="person" class="com.benmao.pojo.Person"/>
科普1:@Nullable代表值可以为空,以防报错空指针异常
public Person(@Nullable String name) {this.name = name;}
如果显示定义了@AutorwiAured的required属性为false,说明这个对象可以为null, 否则就不允许
@Autowired (required=false)private Cat cat;@Autowiredprivate Dog dog;private String name;
科普2:如果bean的id为dog222, 甚至有很多不一样的dog就会报错, 有一个注解可以和@Autowired 搭配使用去专门指定一个bean, 这个注解就是@Qualifier
@Autowired (required=false)@Qualifier(value = "dog222")private Cat cat;@Autowiredprivate Dog dog;private String name;
科普3:用@Resource,也能做到自动装配,功能强大但效率比较低,这里就不介绍这个注解,因为不是很重要,@AutorwiAured的Spring生态更通用
区别:@AutorwiAured默认按 类型(byType)注入,如果存在多个同类型 Bean,需配合 @Qualifier指定名称。 @Resource默认按 名称(byName)注入,如果未指定名称,则回退到按类型注入。
十一、Spring注解开发
在Spring4之后,要使用注解开发,必须要保证aop的包导入了。使用注解需要导入context约束,增加注解的支持(上文已说明)
@Component注解
组件,放在类上,说明这个类被Spring管理了,代表的就是bean
通过component-scan扫描指定的包,可以直接写在实体类中注解说明,不懂看以下代码
<!-- 指定要扫描的包,这个包下的注解就会生效--><context:component-scan base-package="com.benmao.pojo"/><context:annotation-config/><!--不需要写这行代码了--><bean id="user" class="com.benmao.pojo.User"/>
//等价于 <bean id="user" class="com.benmao.pojo.User"/>
@Componentpublic class User {private String name = "Benmao";
}
注意:这里jdk版本过高可能会导致不兼容而报错,我这里报错之后在pom.xml文件修改为jdk11,如果还是不行就得在idea - project structure去修改jdk版本
接下来,如果要注解属性的话,在对于的属性上方使用@Value注解
@Componentpublic class User {//等价于 <property name="name" value="笨猫"/>@Value("笨猫")public String name;
}
同样地,若该实体类包含 set 方法,在 set 方法上添加注解的方式也是相同的
@Componentpublic class User {public String name;@Value("笨猫")public void setName(String name) {this.name = name;}
}
@Component衍生注解
@Component 有几个衍生注解,在web开发中,会按照mvc三层架构分层(dao层、service层、controller层)
dao——@Repository
service——@Service
cotroller——@Controller
这四个注解都是一样的,都是代码将某个类注册到Spring容器中,装配为Bean
xml: 更加万能,适用于任何场合,维护简单方便
注解:不是自己的类使用不了,维护相对复杂
实际开发经验中:xml用来管理bean,注解只负责完成属性的注入,使用的过程中,只需要注意一个问题:必须让注解生效,开启注解的支持(上文已讲这么让注解生效)
十二、使用JavaConfig实现配置
JavaConfig是Spring的一个子项目,Spring4 之后的核心功能
这个就是完全使用Java的方式配置Spring,不需要配置文件,说白了还是注解。我们现在要完全不使用Spring的xml配置了,全权交给Java来做
先来个实体类User,给name注解值为“笨猫”
public class User {private String name;public String getName() {return name;}@Value("笨猫")public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +'}';}
}
接下来是写一个Config配置类,这个配置类有一些注解,其中@Configuration它的底层逻辑注解还是@Component
之前是把bean写在xml配置文件中创建bean对象,在此配置文件中,使用@Bean,具体解释请阅读代码
注意:这里使用@ComponentScan("com.benmao.pojo")注解时可能出现的类文件版本不兼容错误。这个问题与Spring框架版本和JDK版本的兼容性有关。
package com.benmao.config;//把BenmaoConfig这个类注册到容器中,因为它也是一个Component
//@Configuration代表这是一个配置类,就和beans.xml一样
@Configuration//把多个config配置类整合一起
@Import(BenmaoConfig2.class)//显式的指定一个包
@ComponentScan("com.benmao.pojo")
public class BenmaoConfig {//注册一个bean,就相当于一个bean标签,这个方法的名字,就是bena标签id的属性、这个方法的返回值,就是bean标签的class属性@Beanpublic User getUser() {return new User();//就是要返回要注入到bean的对象}
}
最后就是测试类,和以往不同,这是new一个AnnotationConfigApplicationContext类,参数就是所对应的配置文件
public class MyTest {@Testpublic void test() {//如果完全使用了配置方式去做,我们就只能通过 AnnotationConfig 上下文来获取容器,通过配置类的class对象来加载ApplicationContext context = new AnnotationConfigApplicationContext(BenmaoConfig.class);User user = context.getBean("getUser", User.class);System.out.println(user.getName());}
}
小结:这种纯Java的配置方式,在SpringBoot中随处可见!
十二、代理模式
类似于中介、或者演唱会黄牛党,来帮你做一些事情。学习Spring AOP,必须要精通代理模式,否则你搞不懂AOP这个Spring底层实现
对于面试来说,IOC不一定问,但是AOP和SpringMVC是必问的
静态代理
角色分析:
抽象角色:一般使用接口或者抽象类来解决
真实角色:被代理的角色
代理角色:代理真实角色,代理真实角色后,可以做一些附属操作
客户:访问代理对象的人
为什么需要这种设计模式?因为在企业实际开发中,万一修改原来的代码可能导致程序崩溃,这个时候可以创建一个代理类来继承,不仅实现了原有类的作用,而且可以在此基础上自己添加其他的功能(收房租、签合同)
抽象角色类——Rent
package com.benmao.demo01;// 租房
public interface Rent {public void rent();
}
真实角色类——Host
package com.benmao.demo01;// 房东
public class Host implements Rent {public void rent(){System.out.println("房东要出租房子");}
}
代理角色类——Rroxy
package com.benmao.demo01;// 中介
public class Proxy implements Rent{private Host host;public Proxy(){}public Proxy(Host host) {this.host = host;}public void rent(){seetHouse();host.rent();contract();fee();}//看房public void seetHouse() {System.out.println("中介带你看房");}//收中介费public void fee(){System.out.printf("收中介费");}//合同public void contract(){System.out.println("签合同");}
}
客户类——测试类
package com.benmao.demo01;// 需要租房子的客户
public class Client {public static void main(String[] args) {//房东要租房子Host host = new Host();host.rent();System.out.println("----------------------------");//代理,中介帮房东租房子,但是代理角色一般会有一些附属操作Proxy proxy = new Proxy(host);//不用面对房东,直接找房东租房proxy.rent();}
}
代理模式优点:
可以使真实角色的操作更加纯粹,不用关注一些公共的业务
公共也就交给代理角色,实现了业务的分工
公共业务发生扩展的时候,方便集中管理
代理模式缺点:
一个真实角色会产生一个代理角色,如果有很多客户要租房,代码量翻倍,开发效率会变低
动态代理
有什么角色都是一样的,只是动态代理就是解决静态代理的缺点(上文),主要是通过反射来实现。动态代理的代理类是动态生成的,不是我们直接写好的
动态代理分为两大类:①基于接口的动态代理 ②基于类的动态代理
基于接口——JDK动态代理(本文通过这个来讲解)
基于类——cglib
java字节码实现:javassist
需要了解两个类:Proxy-代理, InvocationHandler-调用处理程序
Proxy 和 InvocationHandler 是 Java 中实现 动态代理 的两个核心组件,它们的作用是“在不修改原始类代码的前提下,动态增强其功能”
🎭 Proxy(代理)—— 替身演员
想象你是一个明星/房东(真实对象),但不想亲自处理琐事(如签合同、收礼物)。于是你雇佣了一个替身演员(代理对象)来替你露面
作用:
Proxy 是一个“代工厂”,专门生产替身演员/中介。它根据你指定的“角色要求”(接口),动态创建一个新对象(代理对象),这个对象拥有和明星一样的外在能力(方法),但实际执行的是替身的剧本(增强逻辑)
使用方式:
调用 Proxy.newProxyInstance()
创建代理对象,需要提供三个参数:
ClassLoader:加载代理类的工具(用明星的“化妆师”加载替身);
接口列表:声明替身能模仿明星的哪些能力(如唱歌、演戏);
InvocationHandler:替身的“剧本”(定义替身如何行动)
📜 InvocationHandler(调用处理程序)—— 替身的剧本
替身演员虽然长得像明星,但具体怎么表演需要剧本指导。InvocationHandler 就是这个“剧本”,规定替身遇到粉丝请求时如何应对。
作用:
当粉丝(客户端)要求替身唱歌(调用方法)时,替身不会直接唱,而是翻看剧本(invoke()方法),按剧本的指示行动(如先收钱再唱歌)。
核心方法:invoke() 参数: proxy:替身自己(代理对象); method:粉丝要求的方法(如 sing()); args:方法的参数(如“唱《青花瓷》”)
工作流程: 收粉丝的钱(前置增强:如日志记录); 通知明星本人唱歌(调用真实对象的方法:method.invoke(真实对象, args)); 帮明星收礼物(后置增强:如数据统计 )
抽象角色类——Rent
package com.benmao.demo01;// 租房
public interface Rent {public void rent();
}
真实角色类——Host
package com.benmao.demo01;// 房东
public class Host implements Rent {public void rent(){System.out.println("房东要出租房子");}
}
动态代理类
package com.benmao.demo03;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;//这个类用来自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {// 被代理的接口private Rent rent;public void setRent(Rent rent) {this.rent = rent;}// 生成得到代理类// 参数:① ClassLoader loader, ② Class<?>[] interfaces 需要被代理的类, ③ InvocationHandler h 本身处理类public Object getProxy() {return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(), this );}// 处理代理实例,并返回结果@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {seeHouse();fee();// 动态代理的机制——反射Object res = method.invoke(rent, args);return res;}// 添加其他操作public void seeHouse() {System.out.println("中介带看房子");}public void fee(){System.out.println("收中介费");}
}
客户类——测试类
public class Client {public static void main(String[] args) {// 真实角色Host host = new Host();// 代理角色ProxyInvocationHandler pih = new ProxyInvocationHandler();// 通过调用程序处理角色来处理要调用的接口对象pih.setRent(host);Rent proxy = (Rent) pih.getProxy();//这个类是动态生成的,并没有写,代理类的返回值只能是接口抽象的类,不能是具体的类proxy.rent();}
}
如果动态代理类的代码看不懂,也可以直接把它当作一个工具类来使用,以下是通用代码可供参考
//这个类用来自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {// 被代理的接口private Object target;public void setTarget(Object target) {this.target = target;}// 生成得到代理类// 参数:① ClassLoader loader, ② Class<?>[] interfaces 需要被代理的类, ③ InvocationHandler h 本身处理类public Object getProxy() {return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this );}// 处理代理实例,并返回结果@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {log(method.getName());// 动态代理的机制——反射Object res = method.invoke(target, args);return res;}// 添加其他操作// ....public void log(String msg) {System.out.println("执行了" + msg+"方法");}
}
十三、AOP
AOP的核心价值在于解耦非业务功能与核心业务逻辑。它通过切面模块化通用功能(如日志、事务),再通过动态代理在运行时将功能注入目标位置,实现“不修改源码即可增强功能”。这显著提升了代码的可维护性和复用性,是Spring等框架的基石技术
对于AOP来说,实在理解不了里面的专有名词也无所谓,个人认为会使用即可,在使用的基础上再选择去理解。其实AOP与代理的底层逻辑是类似的,有着“把代码切进去的”思想
要使用AOP,需要导入的依赖
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version>
</dependency>
AOP实现方式一:原生Spring API接口
我们已经导入了所需要的依赖,接下来通过具体代码来讲解如何使用。准备一个用户服务类和它的实现类
//接口
public interface UserService {public void add();public void update();public void delete();public void query();
}//实现类
public class UserServiceImpl implements UserService {@Overridepublic void add() {System.out.println("增加一个用户");}@Overridepublic void update() {System.out.println("修改一个用户");}@Overridepublic void delete() {System.out.println("删除一个用户");}@Overridepublic void query() {System.out.println("查询一个用户");}
}
接下来,我们可以增加一些日志类,一个在方法之前运行的类Log,一个在方法之后运行的类AfterLog(并且AfterLog有方法的返回值)
public class Log implements MethodBeforeAdvice {//method:要执行的目标对象的方法//args: 参数//target: 目标对象@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了");}
}public class AfterLog implements AfterReturningAdvice {//returnValue: 方法返回值,如果这个方法没有返回值就返回null@Overridepublic void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {System.out.println("执行了"+method.getName()+"返回结果为:"+returnValue);}
}
接下来就是在xml配置文件配置AOP的约束了,这点很重要,不然使用不了AOP标签。首先需要知道对哪项功能切入,这里是对UserServiceImpl类中所有的方法切入
<?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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttps://www.springframework.org/schema/aop/spring-aop.xsd"><!--注册bean--><bean id="userService" class="com.benmao.service.UserServiceImpl"/><bean id="log" class="com.benmao.log.Log"/><bean id="aferLog" class="com.benmao.log.AfterLog"/><!--方式一:使用原生Spring API接口--><!--配置AOP: 需要导入aop的约束--><aop:config><!--切入点--><!--expression:表达式 execution():要执行的位置:修饰词、返回值、类名、方法名--><aop:pointcut id="pointcut" expression="execution(* com.benmao.service.UserServiceImpl.*(..))"/><!--执行环绕增强--><aop:advisor advice-ref="log" pointcut-ref="pointcut"/><aop:advisor advice-ref="aferLog" pointcut-ref="pointcut"/></aop:config>
</beans>
MyTest测试类
public class MyTest {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");//动态代理的是接口,这里要用多态的写法UserService userService = context.getBean("userService", UserService.class);userService.add();}
}

AOP实现方式二:自定义类来实现AOP
需要自己写一个自定义类,把这个类里的方法顺序插到别的类去,请看代码解释
//随便创建一个类,定义两个方法
package com.benmao.diy;public class DiyPointCut {public void before() {System.out.println("before");}public void after() {System.out.println("after");}
}
<!-- AOP实现方式二:自定义来实现AOP--><bean id="diy" class="com.benmao.diy.DiyPointCut"/>+<aop:config><!-- 自定义切面:ref就是引用自定义的类--><aop:aspect ref="diy"><!-- 切入点--><aop:pointcut id="point" expression="execution(* com.benmao.service.UserServiceImpl.*(..))"/><!-- 通知--><aop:before method="before" pointcut-ref="point"/><aop:after method="after" pointcut-ref="point"/></aop:aspect></aop:config>
AOP实现方法三:注解实现AOP
只是把xml文件中的标签翻译成注解,简单明了,在一个类中定义,把这个类变成切面
首先需要在xml配置文件创建这个切面类bean
<!-- AOP实现方式三:注解实现AOP--><bean id="diy" class="com.benmao.diy.AnnotationPointCut"/><!-- 开启注解支持: 默认是jdk实现,也可以使用cglib--><aop:aspectj-autoproxy/><!-- 使用jdk--><aop:aspectj-autoproxy proxy-target-class="false"/><!-- 使用cglib--><aop:aspectj-autoproxy proxy-target-class="true"/>
然后可以在这个切面类使用注解了,具体看代码解释
package com.benmao.diy;// 使用注解方式实现AOPimport org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;@Aspect
//这个注解相当于 <aop:aspect ref="这个类">,标注这个类是一个切面
public class AnnotationPointCut {@Before("execution(* com.benmao.service.UserServiceImpl.*(..))")public void before() {System.out.println("before method");}@After("execution(* com.benmao.service.UserServiceImpl.*(..))")public void after() {System.out.println("after method");}//在环绕增强中,可以给定一个参数,代表我们要获取处理的切入点@Around("execution(* com.benmao.service.UserServiceImpl.*(..))")public void around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("环绕前");//执行方法Object proceed = joinPoint.proceed();System.out.println("proceed: " + proceed);//获取方法add()的返回结果System.out.println("环绕后");// Signature signature = joinPoint.getSignature();//获取签名
// System.out.println("signature: " + signature);}
}
运行结果
环绕前
before method
增加一个用户
proceed: 一个返回结果
环绕后
after method
其中"一个返回结果"是add方法,具体看代码或者上文的UserServiceImpl类
十四、整合Mybatis
Mybatis的一般使用
回顾一下Mybatis的用法
步骤一共就三步:导入相关的jar包——编写配置文件——测试
1. 导包
一般需要导入junit、mybatis、mysql数据库、spring相关的、aop、mybatis-spring(新包,mybatis和spring整合的包),具体可以直接用下面写好的依赖,直接导入即可
<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.9</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.3.18</version></dependency><!--Spring操作数据库的话,还需要一个Spring-jdbc--><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.5.RELEASE</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.9.1</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>2.0.6</version></dependency></dependencies>
2. 编写配置文件
首先先回忆一下mybatis框架的用法
第一步:编写实体类
在pojo下随便写个实体类User
package com.benmao.pojo;import lombok.Data;@Data
public class User {private int user_id;private String username;private String password;
}
第二步:编写核心配置文件
首先编写pom.xml文件,注意需要选择合适的版本以免冲突,这里可以直接复制使用。
需要在此文件中写maven静态资源处理问题
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.denlu</groupId><artifactId>Spring-study</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>spring-10-mybatis</artifactId><packaging>war</packaging><name>spring-10-mybatis Maven Webapp</name><url>http://maven.apache.org</url><properties><!-- 显式覆盖父POM的JDK版本(如需用JDK11) --><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!-- 数据库相关 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version> <!-- 支持JDK11的最新稳定版 --></dependency><!-- MyBatis --><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.13</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>2.1.1</version> <!-- 匹配MyBatis 3.5.x --></dependency><!-- Spring --><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.3.29</version> <!-- 统一版本 --></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.3.29</version> <!-- 与webmvc一致 --></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.19</version> <!-- 最新稳定版 --></dependency><!-- Lombok(适配JDK11) --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency><!-- 测试 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency></dependencies><!--maven静态资源过滤问题 --><build><resources><resource><!-- 这个路径就是之后的Mapper.xml文件需要被找到 --><directory>src/main/java</directory><includes><include>**/*.xml</include></includes><filtering>true</filtering></resource></resources></build>
</project>
然后在resource目录下创建mybatis-config.xml配置文件
代码中取别名会自定义把类的名词全部小写当作别名,就是把User类别名成user,之后在Mapper.xml中不需要写完整路径指定User类
然后需要注册mapper接口指定UserMapper类
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><!-- 别名 --><typeAliases><package name="com.benmao.pojo"/></typeAliases><!-- 数据库环境配置 --><environments default="development"><environment id="development"><!-- 事务管理器 --><transactionManager type="JDBC"/><!-- 数据源配置(使用连接池) --><dataSource type="POOLED"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC"/><property name="username" value="root"/><property name="password" value="wjc123456"/><!-- 连接池可选配置 --><property name="poolMaximumActiveConnections" value="10"/><property name="poolMaximumIdleConnections" value="5"/></dataSource></environment></environments><mappers><mapper class="com.benmao.mapper.UserMapper"/></mappers>
</configuration>
第三步:编写mapper接口
在mapper下创建userMapper类
package com.benmao.mapper;import com.benmao.pojo.User;import java.util.List;public interface UserMapper {public List<User> selectUsers();
}
第四步:编写Mapper.xml
同样地,在mapper下写出userMapper对应的userMapper.xml文件,其中id要和类中方法名称一模一样(本人报错找了很久原因...),resultType代表的是返回值,user是User类的别名,你也可以写整个路径
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--绑定接口-->
<mapper namespace="com.benmao.mapper.UserMapper"><select id="selectUsers" resultType="user">select * from test.users;</select>
</mapper>
第五步:测试
import java.io.IOException;
import java.io.InputStream;
import java.util.List;public class MyTest {@Testpublic void test() throws IOException {//加载配置文件String resource = "mybatis-config.xml";InputStream resourceAsStream = Resources.getResourceAsStream(resource);SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession session = build.openSession(true);//获取数据UserMapper mapper = session.getMapper(UserMapper.class);List<User> users = mapper.selectUsers();for (User user : users) {System.out.println(user);}}
}
整合Mybatis方式一
这个就是研究一下spring-mybatis这个依赖包,将Mybatis代码无缝整合到Spring中
需要准备的知识点:熟悉Spring和Mybatis两个框架和它们相关的术语
创建一个spring-dao.xml文件,特别注意的是,代码中导入了在讲解mybatis一般使用中的核心配置文件mybatis-config.xml,之前在这个配置文件注册了mapper类,在spring-dao.xml最后同样需要注册mapper,导致了重复引起报错(又研究了半天这个bug...)
可以通过这个配置文件去对比一下上一节的配置,我们这直接把jdbc的配置写在这,并且直接写了测试类的数据源导入、sqlSessionFactory和sqlSession
<?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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttps://www.springframework.org/schema/aop/spring-aop.xsd"><!--DataSource:使用Spring的数据源替换Mybatis的配置 c3p0 dbcp druid--><!--这里使用Spring提供的JDBC--><bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC"/><property name="username" value="root"/><property name="password" value="wjc123456"/></bean><!--sqlSessionFactory--><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="datasource"/><!--绑定mybatis配置文件--><property name="configLocation" value="classpath:mybatis-config.xml"/><property name="mapperLocations" value="classpath:com/benmao/mapper/*.xml"/></bean><!--sqlSession--><!--sqlSessionTemplate就是sqlSession的写好的模板,本质是一样的--><bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"><!--只能使用构造器注入,因为这个SqlSessionTemplate类没有set方法--><constructor-arg index="0" ref="sqlSessionFactory"/></bean><!--把写好的封装类注册--><bean id="userMapper" class="com.benmao.mapper.UserMapperImpl"><property name="sqlSession" ref="sqlSession"/></bean>
</beans>
创建UserMapper接口的实例UserMapperImpl类,这里使用SqlSessionTemplate模板直接在这就写好之前测试类中的获取数据(上一节)
package com.benmao.mapper;import com.benmao.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;import java.util.List;public class UserMapperImpl implements UserMapper {// 所有操作,以前是使用sqlSession来执行,现在使用sqlSessionTemplateprivate SqlSessionTemplate sqlSession;public void setSqlSession(SqlSessionTemplate sqlSession) {this.sqlSession = sqlSession;}@Overridepublic List<User> selectUsers() {UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> users = mapper.selectUsers();return users;}
}
最后测试一下,写法和之前的Spring一样测试,相当于这些功能都已经整合好了,调用即可。getBean就是在配置文件注册的mapper的bean,注意一定不用循环注册mapper,不然会报错
import com.benmao.mapper.UserMapper;
import com.benmao.pojo.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;import java.util.List;public class MyTest1 {@Testpublic void test1() {ApplicationContext contest = new ClassPathXmlApplicationContext("spring-dao.xml");UserMapper userMapper = contest.getBean("userMapper", UserMapper.class);List<User> users = userMapper.selectUsers();for (User user : users) {System.out.println(user);}}
}
整合Mybatis方式二
其实方法二就是简化方法一,通过继承SqlSessionDaoSupport类给出的Session直接拿来用,具体看代码解释
可以对比UserMapperImpl类,不需要写什么sqlSession的set方法,通过getSqlSession()方法给出的Session直接给出拿来用即可
package com.benmao.mapper;import com.benmao.pojo.User;
import org.mybatis.spring.support.SqlSessionDaoSupport;import java.util.List;public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {@Overridepublic List<User> selectUsers() {//下面两种返回方式是一样的// return getSqlSession().selectList("com.benmao.mapper.UserMapper.selectUsers");return getSqlSession().getMapper(UserMapper.class).selectUsers();}
}
因此在配置文件中,只需要把sqlSessionFactory给这个被注册的UserMapperImpl2就可以了
<bean id="userMapper2" class="com.benmao.mapper.UserMapperImpl2"><property name="sqlSessionFactory" ref="sqlSessionFactory"/></bean>
十五:声明式事务
回顾事务:
把一组业务当成一个业务来做,要么一起成功,要么一起失败
事务在开发十分重要,涉及到数据的一致性
确保完整性和一致性
事务的ACID原则:原子性、一致性、隔离性、持久性
之前在写数据库的操作代码中,同时调用了添加一个用户和删除一个用户的代码,可巧的是删除一个用户代码背后的sql语句是错的,导致了只添加了一个用户,需要被删除用户还活着。这不符合事务的一致性,要成功一起成功,要失败一起失败,删除一个用户会有异常,哪怕添加一个用户是对的,也不会被执行就是一起失败,哦耶~(我没成功谁也别成功)
如何处理,那就是声明式事务
这个就是之前写过AOP知识点spring配置一样,只不过环绕增强把事务写进去就行了,之前环绕增强是写环绕的方法,这块就是事务绑定哪些方法,然后环绕事务
增加两个方法
package com.benmao.mapper;import com.benmao.pojo.User;import java.util.List;public interface UserMapper {public List<User> selectUsers();//添加一个用户public int addUser(User user);//删除一个用户public int deleteUser(int id);
}
然后实现类重写一下,注意看在selectUres方法里,添加用户和删除用户是同时被调用
package com.benmao.mapper;import com.benmao.pojo.User;
import org.mybatis.spring.support.SqlSessionDaoSupport;import java.util.List;public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {@Overridepublic List<User> selectUsers() {User user = new User(6,"123","123321");UserMapper mapper = getSqlSession().getMapper(UserMapper.class);mapper.addUser(user);mapper.deleteUser(1);return mapper.selectUsers();}@Overridepublic int deleteUser(int id) {return getSqlSession().delete("com.benmao.mapper.UserMapper.deleteUser", id);}@Overridepublic int addUser(User user) {return getSqlSession().insert("com.benmao.mapper.UserMapper.addUser", user);}
}
编写SQL语句
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--绑定接口-->
<mapper namespace="com.benmao.mapper.UserMapper"><select id="selectUsers" resultType="user">select * from test.users;</select><insert id="addUser" parameterType="user">insert into test.users (user_id, username, password) values (#{user_id},#{username},#{password});</insert><delete id="deleteUser" parameterType="int">delete from test.users where user_id = #{id};</delete></mapper>
最后在配置文件中最后写上声明式事务标签,然后通过AOP插入,文件头需要写tx的导入(这里就不全部写了),写完就可以测试了
<!--结合AOP实现事务的织入--><!--配置事务通知--><tx:advice id="txAdvice" transaction-manager="transactionManager"><!--给方法配置事务: propagation--><tx:attributes><tx:method name="*" propagation="REQUIRED"/></tx:attributes></tx:advice><!--配置事务切入:AOP的知识点--><aop:config><!--切入点--><aop:pointcut id="txPointCut" expression="execution(* com.benmao.mapper.*.*(..))"/><!--执行环绕增强--><aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/></aop:config>
为什么需要事务:如果不配置事务,可能存在数据提交不一样,在项目开发中十分重要,保证数据的一致性和完整性,添加用户和删除用户要么都成功运行,只要有一个方法有问题导致异常,另一个方法本是可以成功运行,但是由于事务的约束,无法成功,所有只能一起失败